├── contrib └── .gitkeep ├── docs ├── 03_For_Hosts │ ├── 02_Updating.md │ ├── 01_Installation.md │ ├── 06_Authentication.md │ ├── 05_Whitelisting.md │ ├── 07_Customizations.md │ ├── index.md │ └── 04_Heroku_Installation.md ├── 01_General │ ├── 03_Requirements.md │ ├── 04_Screenshots.md │ ├── 01_Project-goals.md │ ├── 02_Contribute.md │ └── 05_FAQ.md ├── images │ ├── fork_button.png │ ├── codespaces_01.png │ ├── codespaces_02.png │ ├── codespaces_03.png │ ├── codespaces_04.png │ ├── codespaces_05.png │ ├── heroku_deploy.png │ ├── rssbridgelogo.png │ ├── bridge_context_named.png │ ├── context_parameter_example.png │ ├── screenshot_rss-bridge_welcome.png │ ├── screenshot_twitterbridge_atom.png │ ├── screenshot_bridgeabstract_example_atom.png │ └── screenshot_bridgeabstract_example_card.png ├── 07_Cache_API │ ├── 01_How_to_create_a_new_cache.md │ ├── 02_CacheInterface.md │ └── index.md ├── 99_Theme │ └── rssbridge │ │ └── config.json ├── readme.md ├── 04_For_Developers │ ├── index.md │ ├── 07_Development_Environment_Setup.md │ ├── 05_Debug_mode.md │ └── 02_Pull_Request_policy.md ├── 10_Bridge_Specific │ ├── Furaffinityuser.md │ ├── FurAffinityBridge.md │ ├── Economist.md │ ├── FacebookBridge.md │ ├── PixivBridge.md │ ├── Substack.md │ ├── TwitterV2.md │ ├── Instagram.md │ └── Vk2.md ├── index.md ├── 09_Technical_recommendations │ └── index.md ├── 05_Bridge_API │ ├── index.md │ └── 01_How_to_create_a_new_bridge.md ├── config.json └── 02_CLI │ └── index.md ├── static ├── favicon.png ├── logo_300px.png ├── logo_600px.png ├── screenshot-1.png ├── screenshot-2.png ├── screenshot-3.png ├── screenshot-4.png ├── screenshot-5.png ├── screenshot-6.png ├── screenshot_rss-bridge_welcome.png ├── screenshot_twitterbridge_atom.png └── connectivity.css ├── lib ├── ActionInterface.php ├── CacheInterface.php ├── config.php ├── Debug.php ├── Container.php ├── php8backports.php ├── php-urljoin │ └── LICENSE ├── parsedown │ └── LICENSE.txt ├── simplehtmldom │ └── LICENSE ├── RssBridge.php ├── FormatAbstract.php ├── bootstrap.php └── FormatFactory.php ├── middlewares ├── Middleware.php ├── MaintenanceMiddleware.php ├── SecurityMiddleware.php ├── TokenAuthenticationMiddleware.php └── BasicAuthMiddleware.php ├── .git-blame-ignore-revs ├── config ├── php.ini ├── nginx.conf └── php-fpm.conf ├── docker-compose.yml ├── .devcontainer ├── xdebug.ini ├── nginx.conf ├── devcontainer.json └── launch.json ├── bridges ├── Rule34Bridge.php ├── KonachanBridge.php ├── YandereBridge.php ├── LolibooruBridge.php ├── MilbooruBridge.php ├── XbooruBridge.php ├── MspabooruBridge.php ├── TbibBridge.php ├── SafebooruBridge.php ├── ReleasesSwitchBridge.php ├── ListverseBridge.php ├── KoreusBridge.php ├── SteamGroupAnnouncementsBridge.php ├── FeedExpanderTestBridge.php ├── TheWhiteboardBridge.php ├── Kanali6Bridge.php ├── CourrierInternationalBridge.php ├── NFLRUSBridge.php ├── CommonDreamsBridge.php ├── StripeAPIChangeLogBridge.php ├── VarietyBridge.php ├── WYMTNewsBridge.php ├── OglafBridge.php ├── EstCeQuonMetEnProdBridge.php ├── ComboiosDePortugalBridge.php ├── FreeCodeCampBridge.php ├── FliegermagazinBridge.php ├── DansTonChatBridge.php ├── TheRedHandFilesBridge.php ├── BleepingComputerBridge.php ├── JohannesBlickBridge.php ├── Rule34pahealBridge.php ├── AcrimedBridge.php ├── EngadgetBridge.php ├── QwerteeBridge.php ├── BastaBridge.php ├── LogicMastersBridge.php ├── NiceMatinBridge.php ├── FabriceBellardBridge.php ├── QwantzBridge.php ├── UsesTechBridge.php ├── MondeDiploBridge.php ├── BinanceBridge.php ├── PCGWNewsBridge.php ├── LuftsportSHBridge.php ├── MozillaSecurityBridge.php ├── BundesverbandFuerFreieKammernBridge.php ├── DeutscherAeroClubBridge.php ├── N26Bridge.php ├── HanimeBridge.php ├── LesJoiesDuCodeBridge.php ├── RainLoopBridge.php ├── Shimmie2Bridge.php ├── TheYeteeBridge.php ├── ExecuteProgramBridge.php ├── OpenwrtSecurityBridge.php ├── QnapBridge.php ├── CopieDoubleBridge.php ├── TeefuryBridge.php ├── CryptomeBridge.php ├── ScoopItBridge.php ├── GoAccessBridge.php ├── RiptApparelBridge.php ├── SpottschauBridge.php ├── ParksOnTheAirBridge.php ├── PicartoBridge.php ├── MsnMondeBridge.php ├── ForGifsBridge.php ├── SymfonyCastsBridge.php ├── ViceBridge.php ├── ViadeoCompanyBridge.php ├── CBCEditorsBlogBridge.php ├── GithubPullRequestBridge.php ├── BlaguesDeMerdeBridge.php ├── NikonDownloadCenterBridge.php ├── EASeedBridge.php ├── LaTeX3ProjectNewslettersBridge.php ├── NYTBridge.php ├── Rue89Bridge.php ├── LeMondeInformatiqueBridge.php ├── StanfordSIRbookreviewBridge.php ├── KhinsiderBridge.php ├── RaceDepartmentBridge.php ├── FirefoxReleaseNotesBridge.php ├── ScmbBridge.php ├── TebeoBridge.php ├── PanacheDigitalGamesBridge.php ├── FiaBridge.php ├── EDDHPresseschauBridge.php ├── VproTegenlichtBridge.php ├── TheFarSideBridge.php ├── IdenticaBridge.php ├── WallmineNewsBridge.php ├── PlantUMLReleasesBridge.php ├── SuperSmashBlogBridge.php ├── DuckDuckGoBridge.php ├── TinyLetterBridge.php ├── WallpaperflareBridge.php ├── SummitsOnTheAirBridge.php ├── ABCNewsBridge.php ├── ElsevierBridge.php ├── SleeperFantasyFootballBridge.php └── ManyVidsBridge.php ├── templates ├── error.html.php ├── bridge-error.html.php ├── token.html.php ├── base.html.php ├── connectivity.html.php └── frontpage.html.php ├── app.json ├── bin ├── cache-clear ├── test └── cache-prune ├── actions ├── HealthAction.php ├── ListAction.php └── DetectAction.php ├── formats ├── PlaintextFormat.php └── SfeedFormat.php ├── docker-bake.hcl ├── caches ├── NullCache.php └── ArrayCache.php └── UNLICENSE /contrib/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/03_For_Hosts/02_Updating.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/01_General/03_Requirements.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | - PHP 7.4 (or higher) 4 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dethos/rss-bridge/HEAD/static/favicon.png -------------------------------------------------------------------------------- /static/logo_300px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dethos/rss-bridge/HEAD/static/logo_300px.png -------------------------------------------------------------------------------- /static/logo_600px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dethos/rss-bridge/HEAD/static/logo_600px.png -------------------------------------------------------------------------------- /docs/03_For_Hosts/01_Installation.md: -------------------------------------------------------------------------------- 1 | https://github.com/RSS-Bridge/rss-bridge/blob/master/README.md 2 | -------------------------------------------------------------------------------- /static/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dethos/rss-bridge/HEAD/static/screenshot-1.png -------------------------------------------------------------------------------- /static/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dethos/rss-bridge/HEAD/static/screenshot-2.png -------------------------------------------------------------------------------- /static/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dethos/rss-bridge/HEAD/static/screenshot-3.png -------------------------------------------------------------------------------- /static/screenshot-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dethos/rss-bridge/HEAD/static/screenshot-4.png -------------------------------------------------------------------------------- /static/screenshot-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dethos/rss-bridge/HEAD/static/screenshot-5.png -------------------------------------------------------------------------------- /static/screenshot-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dethos/rss-bridge/HEAD/static/screenshot-6.png -------------------------------------------------------------------------------- /docs/images/fork_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dethos/rss-bridge/HEAD/docs/images/fork_button.png -------------------------------------------------------------------------------- /docs/images/codespaces_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dethos/rss-bridge/HEAD/docs/images/codespaces_01.png -------------------------------------------------------------------------------- /docs/images/codespaces_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dethos/rss-bridge/HEAD/docs/images/codespaces_02.png -------------------------------------------------------------------------------- /docs/images/codespaces_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dethos/rss-bridge/HEAD/docs/images/codespaces_03.png -------------------------------------------------------------------------------- /docs/images/codespaces_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dethos/rss-bridge/HEAD/docs/images/codespaces_04.png -------------------------------------------------------------------------------- /docs/images/codespaces_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dethos/rss-bridge/HEAD/docs/images/codespaces_05.png -------------------------------------------------------------------------------- /docs/images/heroku_deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dethos/rss-bridge/HEAD/docs/images/heroku_deploy.png -------------------------------------------------------------------------------- /docs/images/rssbridgelogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dethos/rss-bridge/HEAD/docs/images/rssbridgelogo.png -------------------------------------------------------------------------------- /docs/images/bridge_context_named.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dethos/rss-bridge/HEAD/docs/images/bridge_context_named.png -------------------------------------------------------------------------------- /static/screenshot_rss-bridge_welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dethos/rss-bridge/HEAD/static/screenshot_rss-bridge_welcome.png -------------------------------------------------------------------------------- /static/screenshot_twitterbridge_atom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dethos/rss-bridge/HEAD/static/screenshot_twitterbridge_atom.png -------------------------------------------------------------------------------- /docs/images/context_parameter_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dethos/rss-bridge/HEAD/docs/images/context_parameter_example.png -------------------------------------------------------------------------------- /docs/images/screenshot_rss-bridge_welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dethos/rss-bridge/HEAD/docs/images/screenshot_rss-bridge_welcome.png -------------------------------------------------------------------------------- /docs/images/screenshot_twitterbridge_atom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dethos/rss-bridge/HEAD/docs/images/screenshot_twitterbridge_atom.png -------------------------------------------------------------------------------- /lib/ActionInterface.php: -------------------------------------------------------------------------------- 1 | img/favicon.png", 3 | "js": [ 4 | "js/daux.min.js" 5 | ], 6 | "css": [ 7 | "css/theme.min.css" 8 | ] 9 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | rss-bridge: 4 | image: rssbridge/rss-bridge:latest 5 | volumes: 6 | - ./config:/config 7 | ports: 8 | - 3000:80 9 | restart: unless-stopped 10 | -------------------------------------------------------------------------------- /.devcontainer/xdebug.ini: -------------------------------------------------------------------------------- 1 | [xdebug] 2 | xdebug.mode=develop,debug 3 | xdebug.client_host=localhost 4 | xdebug.client_port=9003 5 | xdebug.start_with_request=yes 6 | xdebug.discover_client_host=false 7 | xdebug.log='/var/www/html/xdebug.log' -------------------------------------------------------------------------------- /bridges/Rule34Bridge.php: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 |

9 | 10 |

11 | 12 | 13 |

14 | 15 |

16 | -------------------------------------------------------------------------------- /bridges/MilbooruBridge.php: -------------------------------------------------------------------------------- 1 | clear(); 17 | -------------------------------------------------------------------------------- /docs/07_Cache_API/02_CacheInterface.md: -------------------------------------------------------------------------------- 1 | See `CacheInterface`. 2 | 3 | ```php 4 | interface CacheInterface 5 | { 6 | public function get(string $key, $default = null); 7 | 8 | public function set(string $key, $value, int $ttl = null): void; 9 | 10 | public function delete(string $key): void; 11 | 12 | public function clear(): void; 13 | 14 | public function prune(): void; 15 | } 16 | ``` 17 | -------------------------------------------------------------------------------- /lib/config.php: -------------------------------------------------------------------------------- 1 | 200, 11 | 'message' => 'all is good', 12 | ]; 13 | return new Response(Json::encode($response), 200, ['content-type' => 'application/json']); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /formats/PlaintextFormat.php: -------------------------------------------------------------------------------- 1 | getFeed(); 10 | foreach ($this->getItems() as $item) { 11 | $feed['items'][] = $item->toArray(); 12 | } 13 | $text = print_r($feed, true); 14 | return $text; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /templates/bridge-error.html.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

13 | 14 |

-------------------------------------------------------------------------------- /docker-bake.hcl: -------------------------------------------------------------------------------- 1 | target "docker-metadata-action" {} 2 | 3 | group "default" { 4 | targets = ["image-local"] 5 | } 6 | 7 | target "image" { 8 | inherits = ["docker-metadata-action"] 9 | } 10 | 11 | target "image-local" { 12 | inherits = ["image"] 13 | output = ["type=docker"] 14 | } 15 | 16 | target "image-all" { 17 | inherits = ["image"] 18 | platforms = [ 19 | "linux/amd64", 20 | "linux/arm64", 21 | "linux/arm/v7" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /templates/token.html.php: -------------------------------------------------------------------------------- 1 | 7 | 8 |

9 | Authentication with token required 10 |

11 | 12 |

13 | 14 |

15 | 16 |
17 | 18 | 19 | 20 |
21 | -------------------------------------------------------------------------------- /bridges/XbooruBridge.php: -------------------------------------------------------------------------------- 1 | getURI() . 'thumbnails/' . $element->directory 13 | . '/thumbnail_' . $element->hash . '.jpg'; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /bridges/MspabooruBridge.php: -------------------------------------------------------------------------------- 1 | getURI() . 'thumbnails/' . $element->directory 13 | . '/thumbnail_' . $element->image; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /docs/07_Cache_API/index.md: -------------------------------------------------------------------------------- 1 | A _Cache_ is a class that allows **RSS-Bridge** to store fetched data in a local storage area on the server. 2 | Cache imlementations are placed in the `caches/` folder (see [Folder structure](../04_For_Developers/03_Folder_structure.md)). 3 | A cache must implement the [`CacheInterface`](../07_Cache_API/02_CacheInterface.md) interface. 4 | 5 | For more information about how to create a new `Cache`, read 6 | [How to create a new cache?](../07_Cache_API/01_How_to_create_a_new_cache.md) 7 | -------------------------------------------------------------------------------- /.devcontainer/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 3100 default_server; 3 | root /workspaces/rss-bridge; 4 | access_log /var/log/nginx/rssbridge.access.log; 5 | error_log /var/log/nginx/rssbridge.error.log; 6 | index index.php; 7 | 8 | location ~ /(\.|vendor|tests) { 9 | deny all; 10 | return 403; # Forbidden 11 | } 12 | 13 | location ~ \.php$ { 14 | include snippets/fastcgi-php.conf; 15 | fastcgi_pass unix:/var/run/php/php8.2-fpm.sock; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /bridges/TbibBridge.php: -------------------------------------------------------------------------------- 1 | getURI() . 'thumbnails/' . $element->directory 14 | . '/thumbnail_' . preg_replace($regex, '.jpg', $element->image); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | debug('This is a test debug message'); 17 | 18 | $logger->info('This is a test info message'); 19 | 20 | $logger->error('This is a test error message'); 21 | -------------------------------------------------------------------------------- /caches/NullCache.php: -------------------------------------------------------------------------------- 1 | getURI() . 'thumbnails/' . $element->directory 14 | . '/thumbnail_' . preg_replace($regex, '.jpg', $element->image); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /config/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | listen [::]:80 default_server; 4 | root /app; 5 | access_log /var/log/nginx/access.log; 6 | error_log /var/log/nginx/error.log; 7 | index index.php; 8 | 9 | location ~ /(\.|vendor|tests) { 10 | deny all; 11 | return 403; # Forbidden 12 | } 13 | 14 | location ~ \.php$ { 15 | include snippets/fastcgi-php.conf; 16 | fastcgi_read_timeout 45s; 17 | fastcgi_pass unix:/var/run/php/php8.2-fpm.sock; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /bridges/ReleasesSwitchBridge.php: -------------------------------------------------------------------------------- 1 | collectDataUrl(self::URI . 'xml.php'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/01_General/01_Project-goals.md: -------------------------------------------------------------------------------- 1 | **RSS-Bridge** aims at sites that: 2 | 3 | - don't provide public accessible Atom or RSS feeds 4 | - force their users to subscribe to e-mail notifications 5 | - force their users to use their own proprietary APIs 6 | - require their users to come back on a regular basis in order to check for new content 7 | 8 | **RSS-Bridge** will generate feeds based on "bridges" that are developed for any site. Those bridges will collect data and extract all necessary information which is then converted into various feed formats like Atom or RSS. -------------------------------------------------------------------------------- /docs/04_For_Developers/index.md: -------------------------------------------------------------------------------- 1 | This area is intended for developers who decide to contribute to **RSS-Bridge**. 2 | 3 | It is written in PHP. 4 | 5 | If you are new to **RSS-Bridge** you should make yourself familiar with some general aspects: 6 | 7 | - [Coding style policy](./01_Coding_style_policy.md) 8 | - [Folder structure](./03_Folder_structure.md) 9 | - [Debug mode](./05_Debug_mode.md) 10 | - [Bridge API](../05_Bridge_API/index.md) 11 | - [Cache API](../07_Cache_API/index.md) 12 | - [Technical recommendations](../09_Technical_recommendations/index.md) 13 | -------------------------------------------------------------------------------- /middlewares/MaintenanceMiddleware.php: -------------------------------------------------------------------------------- 1 | '503 Service Unavailable', 14 | 'message' => 'RSS-Bridge is down for maintenance.', 15 | ]), 503); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /config/php-fpm.conf: -------------------------------------------------------------------------------- 1 | ; Inspired by https://github.com/docker-library/php/blob/master/8.2/bookworm/fpm/Dockerfile 2 | 3 | [global] 4 | error_log = /proc/self/fd/2 5 | 6 | ; https://github.com/docker-library/php/pull/725#issuecomment-443540114 7 | log_limit = 8192 8 | 9 | [www] 10 | ; php-fpm closes STDOUT on startup, so sending logs to /proc/self/fd/1 does not work. 11 | ; https://bugs.php.net/bug.php?id=73886 12 | access.log = /proc/self/fd/2 13 | 14 | clear_env = no 15 | 16 | ; Ensure worker stdout and stderr are sent to the main error log. 17 | catch_workers_output = yes 18 | decorate_workers_output = no 19 | -------------------------------------------------------------------------------- /docs/04_For_Developers/07_Development_Environment_Setup.md: -------------------------------------------------------------------------------- 1 | 2 | ## Docs with Docker 3 | 4 | If you want to edit the docs add this to your docker-compose.yml: 5 | 6 | ```yml 7 | services: 8 | [...] 9 | 10 | daux: 11 | image: daux/daux.io 12 | ports: 13 | - 8085:8085 14 | working_dir: /build 15 | volumes: 16 | - ./rss-bridge/docs:/build/docs 17 | network_mode: host 18 | ``` 19 | 20 | and run for example the `daux serve` command with `docker-compose run --rm daux daux serve`. 21 | After that you can access the docs at `localhost:8085` and edit the files in `rss-bridge/docs`. 22 | -------------------------------------------------------------------------------- /docs/03_For_Hosts/05_Whitelisting.md: -------------------------------------------------------------------------------- 1 | Modify `config.ini.php` to limit available bridges. Those changes should be applied in the `[system]` section. 2 | 3 | ## Enable all bridges 4 | 5 | ``` 6 | [system] 7 | 8 | enabled_bridges[] = * 9 | ``` 10 | 11 | ## Enable some bridges 12 | 13 | ``` 14 | [system] 15 | 16 | enabled_bridges[] = TwitchBridge 17 | enabled_bridges[] = GettrBridge 18 | ``` 19 | 20 | ## Enable all bridges (legacy shortcut) 21 | 22 | ``` 23 | echo '*' > whitelist.txt 24 | ``` 25 | 26 | ## Enable some bridges (legacy shortcut) 27 | 28 | ``` 29 | echo -e "TwitchBridge\nTwitterBridge" > whitelist.txt 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/01_General/02_Contribute.md: -------------------------------------------------------------------------------- 1 | There are many things you can do to contribute to **RSS-Bridge** as developer or as user without any knowledge in PHP or (web) development. Here are a few things: 2 | 3 | - Share **RSS-Bridge** with your friends (Twitter, Facebook, ..._you name it_...) 4 | - Report broken bridges or bugs [here](https://github.com/RSS-Bridge/rss-bridge/issues) 5 | - Request new features or propose ideas (via [Issues](https://github.com/RSS-Bridge/rss-bridge/issues)) 6 | - Discuss bugs, features, ideas or [issues](https://github.com/RSS-Bridge/rss-bridge/issues) 7 | - Add new bridges or improve the API 8 | - Improve this documentation 9 | - Host **RSS-Bridge** -------------------------------------------------------------------------------- /lib/Debug.php: -------------------------------------------------------------------------------- 1 | prune(); 25 | -------------------------------------------------------------------------------- /middlewares/SecurityMiddleware.php: -------------------------------------------------------------------------------- 1 | toArray() as $key => $value) { 13 | if (!is_string($value)) { 14 | return new Response(render(__DIR__ . '/../templates/error.html.php', [ 15 | 'message' => "Query parameter \"$key\" is not a string.", 16 | ]), 400); 17 | } 18 | } 19 | return $next($request); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /bridges/ListverseBridge.php: -------------------------------------------------------------------------------- 1 | collectExpandableDatas('https://listverse.com/feed/', 15); 14 | } 15 | 16 | protected function parseItem(array $item) 17 | { 18 | $dom = getSimpleHTMLDOM($item['uri']); 19 | $article = $dom->find('#articlecontentonly', 0); 20 | $item['content'] = $article; 21 | return $item; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /bridges/KoreusBridge.php: -------------------------------------------------------------------------------- 1 | find('p.itemText', 0)->innertext; 14 | $item['content'] = utf8_encode($text); 15 | 16 | return $item; 17 | } 18 | 19 | public function collectData() 20 | { 21 | $this->collectExpandableDatas('https://feeds.feedburner.com/Koreus-articles'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /bridges/SteamGroupAnnouncementsBridge.php: -------------------------------------------------------------------------------- 1 | [ 13 | 'name' => 'Group name', 14 | 'exampleValue' => 'freegamesfinders', 15 | 'required' => true 16 | ] 17 | ] 18 | ]; 19 | 20 | public function collectData() 21 | { 22 | $uri = self::URI . 'groups/' . $this->getInput('g') . '/rss'; 23 | $this->collectExpandableDatas($uri, 10); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /bridges/FeedExpanderTestBridge.php: -------------------------------------------------------------------------------- 1 | collectExpandableDatas($url); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /bridges/TheWhiteboardBridge.php: -------------------------------------------------------------------------------- 1 | find('center', 1)->find('img', 0); 17 | $image->src = self::URI . '/' . $image->src; 18 | 19 | $item['title'] = explode("\r\n", $html->find('center', 1)->plaintext)[0]; 20 | $item['content'] = $image; 21 | $item['timestamp'] = explode("\r\n", $html->find('center', 1)->plaintext)[0]; 22 | 23 | $this->items[] = $item; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /docs/10_Bridge_Specific/Furaffinityuser.md: -------------------------------------------------------------------------------- 1 | # FuraffinityuserBridge 2 | 3 | ## How to retrieve and use cookie values 4 | --- 5 | > The following steps describe how to get the session cookies using a Chromium-based browser. Other browser may require slightly different steps. Keyword search "how to find cookie values" in your favorite search engine. 6 | 7 | ### Retreiving session cookies. 8 | 9 | - Login to Furaffinity 10 | 11 | - Open DevTools by pressing F12 12 | 13 | - Open "Application" 14 | 15 | - On the left side, select "Cookies" -> "Furaffinity.net" 16 | 17 | - There will be (at least) two cookie informations in the main window. You need the values of the "a" key and "b" key 18 | 19 | ### Configuring RSS-Bridge 20 | 21 | - Copy/Paste the values from cookies "a" and "b" into their respective fields in the bridge config and generate the feed -------------------------------------------------------------------------------- /docs/10_Bridge_Specific/FurAffinityBridge.md: -------------------------------------------------------------------------------- 1 | FurAffinityBridge 2 | =============== 3 | By default this bridge will only return submissions that are rated "General" and are public. 4 | 5 | To unlock the ability to load submissions that require an account to view or are rated "Mature" and higher, you must set the following in `config.ini.php` with cookies from an existing FurAffinity account with the desired maturity ratings enabled in [Account Settings](https://www.furaffinity.net/controls/settings/). 6 | 7 | ``` 8 | [FurAffinityBridge] 9 | aCookie = "your-a-cookie-value-here" ; from cookie "a" 10 | bCookie = "your-b-cookie-value-here" ; from cookie "b" 11 | ``` 12 | 13 | To confirm the bridge is authenticated, the name of the authenticating account will be shown in the bridge's name once the bridge has been used at least once. (Example: `user's FurAffinity Bridge`) 14 | -------------------------------------------------------------------------------- /bridges/Kanali6Bridge.php: -------------------------------------------------------------------------------- 1 | collectExpandableDatas(static::URI . 'feed/all/rss.xml', 20); 14 | } 15 | 16 | protected function parseItem(array $item) 17 | { 18 | $articlePage = getSimpleHTMLDOMCached($item['uri']); 19 | $content = $articlePage->find('.article-text, depeche-text', 0); 20 | if (!$content) { 21 | return $item; 22 | } 23 | $item['content'] = sanitize($content); 24 | 25 | return $item; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docs/10_Bridge_Specific/Economist.md: -------------------------------------------------------------------------------- 1 | # EconomistWorldInBriefBridge and EconomistBridge 2 | 3 | In May 2024, The Economist finally fixed its paywall, and it started requiring authorization. Which means you can't use this bridge unless you have an active subscription. 4 | 5 | If you do, the way to use the bridge is to snitch a cookie: 6 | 1. Log in to The Economist 7 | 2. Open DevTools (Chrome DevTools or Firefox Developer Tools) 8 | 2. Go to https://www.economist.com/the-world-in-brief 9 | 3. In DevTools, go to the "Network" tab, there select the first request (`the-world-in-brief`) and copy the value of the `Cookie:` header from "Request Headers". 10 | 11 | The cookie lives three months. 12 | 13 | Once you've done this, add the cookie to your `config.ini.php`: 14 | 15 | ``` 16 | [EconomistWorldInBriefBridge] 17 | cookie = "" 18 | 19 | [EconomistBridge] 20 | cookie = "" 21 | ``` 22 | -------------------------------------------------------------------------------- /bridges/NFLRUSBridge.php: -------------------------------------------------------------------------------- 1 | find('.big-post_content-col'); 16 | 17 | foreach ($articles as $article) { 18 | $item = []; 19 | 20 | $url = $article->find('.big-post_title.card-title a', 0); 21 | 22 | $item['uri'] = $url->href; 23 | $item['title'] = $url->plaintext; 24 | $item['content'] = $article->find('div', 0)->innertext; 25 | $this->items[] = $item; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docs/03_For_Hosts/index.md: -------------------------------------------------------------------------------- 1 | This section is directed at **hosts** and **server administrators**. 2 | 3 | RSS-Bridge comes with a large amount of bridges. 4 | 5 | Some bridges could be implemented more efficiently by actually using proprietary APIs, 6 | but there are reasons against it: 7 | 8 | - RSS-Bridge exists in the first place to NOT use APIs. 9 | - See [the rant](https://github.com/RSS-Bridge/rss-bridge/blob/master/README.md#Rant) 10 | 11 | - APIs require private keys that could be stored on servers running RSS-Bridge, 12 | - which is a security concern, involves complex authorizations for inexperienced users and could cause harm (when using paid services for example). In a closed environment (a server only you use for yourself) however you might be interested in using them anyway. So, check [this](https://github.com/RSS-Bridge/rss-bridge/pull/478/files) possible implementation of an anti-captcha solution. 13 | -------------------------------------------------------------------------------- /bridges/CommonDreamsBridge.php: -------------------------------------------------------------------------------- 1 | collectExpandableDatas('http://www.commondreams.org/rss.xml', 10); 13 | } 14 | 15 | protected function parseItem(array $item) 16 | { 17 | $item['content'] = $this->extractContent($item['uri']); 18 | return $item; 19 | } 20 | 21 | private function extractContent($url) 22 | { 23 | $dom = getSimpleHTMLDOMCached($url); 24 | $summary = $dom->find('div.node__body', 0); 25 | $text = $summary->innertext; 26 | $dom->clear(); 27 | unset($dom); 28 | return $text; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /docs/04_For_Developers/05_Debug_mode.md: -------------------------------------------------------------------------------- 1 |

Warning!

2 | 3 | Enabling debug mode on a public server may result in malicious clients retrieving sensitive data about your server and possibly gaining access to it. 4 | Do not enable debug mode on a public server, unless you understand the implications of your doing! 5 | 6 | *** 7 | 8 | Debug mode enables error reporting and prevents loading data from the cache (data is still written to the cache). 9 | To enable debug mode, set in `config.ini.php`: 10 | 11 | enable_debug_mode = true 12 | 13 | Allow only explicit ip addresses: 14 | 15 | debug_mode_whitelist[] = 127.0.0.1 16 | debug_mode_whitelist[] = 192.168.1.10 17 | 18 | _Notice_: 19 | 20 | * An empty file enables debug mode for anyone! 21 | * The bridge whitelist still applies! (debug mode does **not** enable all bridges) 22 | 23 | RSS-Bridge will give you a visual feedback when debug mode is enabled. 24 | -------------------------------------------------------------------------------- /bridges/StripeAPIChangeLogBridge.php: -------------------------------------------------------------------------------- 1 | find('h3') as $change) { 16 | $item = []; 17 | $item['title'] = trim($change->plaintext); 18 | $item['uri'] = self::URI . '#' . $item['title']; 19 | $item['author'] = 'stripe'; 20 | $item['content'] = $change->nextSibling()->outertext; 21 | $item['timestamp'] = strtotime($item['title']); 22 | $this->items[] = $item; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | RSS-Bridge is a web application. 2 | 3 | It generates web feeds for websites that don't have one. 4 | 5 | Officially hosted instance: https://rss-bridge.org/bridge01/ 6 | 7 | - You want to know more about **RSS-Bridge**? 8 | Check out our **[project goals](01_General/01_Project-goals.md)**. 9 | 10 | - You want to contribute and don't know how? 11 | Check out our **[How can I contribute?](01_General/02_Contribute.md)** section. 12 | 13 | - You are a developer and searching for more details? 14 | Check out our **[For developers](04_For_Developers/index.md)** section. 15 | 16 | - You want to know what is required to host **RSS-Bridge**? 17 | Check out the **[Requirements](01_General/03_Requirements.md)** section. 18 | 19 | - You want to host **RSS-Bridge**? 20 | Check out the **[For hosts](03_For_Hosts/index.md)** section. 21 | 22 | - You have questions? 23 | Check out the **[FAQ](01_General/05_FAQ.md)**. -------------------------------------------------------------------------------- /lib/Container.php: -------------------------------------------------------------------------------- 1 | values[$offset] = $value; 13 | } 14 | 15 | #[\ReturnTypeWillChange] 16 | public function offsetGet($offset) 17 | { 18 | if (!isset($this->values[$offset])) { 19 | throw new \Exception(sprintf('Unknown container key: "%s"', $offset)); 20 | } 21 | if (!isset($this->resolved[$offset])) { 22 | $this->resolved[$offset] = $this->values[$offset]($this); 23 | } 24 | return $this->resolved[$offset]; 25 | } 26 | 27 | #[\ReturnTypeWillChange] 28 | public function offsetExists($offset) 29 | { 30 | } 31 | 32 | public function offsetUnset($offset): void 33 | { 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/php8backports.php: -------------------------------------------------------------------------------- 1 | collectExpandableDatas('https://feeds.feedburner.com/variety/headlines', 15); 14 | } 15 | 16 | protected function parseItem(array $item) 17 | { 18 | $articlePage = getSimpleHTMLDOM($item['uri']); 19 | 20 | // Remove Script tags 21 | foreach ($articlePage->find('script') as $script_tag) { 22 | $script_tag->remove(); 23 | } 24 | $article = $articlePage->find('div.c-featured-media', 0); 25 | $article = $article . $articlePage->find('.c-content', 0); 26 | 27 | $item['content'] = $article; 28 | 29 | return $item; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /bridges/WYMTNewsBridge.php: -------------------------------------------------------------------------------- 1 | find('.card-body'); 16 | 17 | foreach ($articles as $article) { 18 | $item = []; 19 | $url = $article->find('.headline a', 0); 20 | $item['uri'] = $url->href; 21 | $item['title'] = trim($url->plaintext); 22 | $item['author'] = $article->find('.author', 0)->plaintext; 23 | $item['content'] = $article->find('.deck', 0)->plaintext; 24 | $this->items[] = $item; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docs/10_Bridge_Specific/FacebookBridge.md: -------------------------------------------------------------------------------- 1 | FacebookBridge 2 | =============== 3 | State of this bridge: 4 | - Facebook Groups (and probably other sections too) do not work at all 5 | - No maintainer 6 | - Needs cookie consent support for public pages 7 | - Needs login support see [this example](https://github.com/RSS-Bridge/rss-bridge/issues/1891) for Instagram) for private groups 8 | 9 | Due to the 2020 [Facebook redesign](https://engineering.fb.com/2020/05/08/web/facebook-redesign/) 10 | and the requirement to [accept cookies](https://www.facebook.com/business/help/348535683460989) 11 | users are getting [problems with Facebook on public RSS-Bridge instances](https://github.com/RSS-Bridge/rss-bridge/issues/2047). 12 | 13 | Relevant Info 14 | -------------- 15 | 16 | - [Facebook Cookies](https://www.facebook.com/policy/cookies/) 17 | - "Datr" is a unique identifier for your browser and it has a lifespan of two years. 18 | - "c_user" and "xs" cookies to verify the account and have a lifespan of 365 days 19 | -------------------------------------------------------------------------------- /templates/base.html.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RSS-Bridge 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 | 19 | 20 |
21 | 22 | 23 |
24 | 25 |
26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /bridges/OglafBridge.php: -------------------------------------------------------------------------------- 1 | [ 12 | 'name' => 'limit (max 20)', 13 | 'type' => 'number', 14 | 'defaultValue' => 10, 15 | 'required' => true, 16 | ] 17 | ] 18 | ]; 19 | 20 | public function collectData() 21 | { 22 | $url = self::URI . 'feeds/rss/'; 23 | $limit = min(20, $this->getInput('limit')); 24 | $this->collectExpandableDatas($url, $limit); 25 | } 26 | 27 | protected function parseItem($item) 28 | { 29 | $html = getSimpleHTMLDOMCached($item['uri']); 30 | $comicImage = $html->find('img[id="strip"]', 0); 31 | $item['content'] = $comicImage; 32 | 33 | return $item; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /docs/09_Technical_recommendations/index.md: -------------------------------------------------------------------------------- 1 | ## General recommendations 2 | 3 | ## Test a site before building a bridge 4 | 5 | Some sites make use of anti-bot mechanisms (e.g.: by using JavaScript) in which case they work fine in regular browsers, 6 | but not in the PHP environment. 7 | 8 | To check if a site works with RSS-Bridge, create a new bridge using the 9 | [template](../05_Bridge_API/02_BridgeAbstract.md#template) 10 | and load a valid URL (not the base URL!). 11 | 12 | **Example (using github.com)** 13 | 14 | ```PHP 15 | getURI() . '#' . date('Y-m-d'); 17 | $item['title'] = $this->getName(); 18 | $item['author'] = 'Nicolas Hoffmann'; 19 | $item['timestamp'] = strtotime('today midnight'); 20 | $item['content'] = str_replace( 21 | 'src="/', 22 | 'src="' . self::URI, 23 | trim(extractFromDelimiters($html->outertext, '', '
find('div.item') as $element) { 16 | $item = []; 17 | $item['uri'] = $element->find('a', 0)->href; 18 | $titleContent = $element->find('h3 a', 0); 19 | if ($titleContent) { 20 | $item['title'] = 'DansTonChat ' . html_entity_decode($titleContent->plaintext, ENT_QUOTES); 21 | } else { 22 | $item['title'] = 'DansTonChat'; 23 | } 24 | $item['content'] = $element->find('div.item-content a', 0)->innertext; 25 | $this->items[] = $item; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /middlewares/TokenAuthenticationMiddleware.php: -------------------------------------------------------------------------------- 1 | withAttribute('token', $request->get('token')); 15 | 16 | if (! $request->attribute('token')) { 17 | return new Response(render(__DIR__ . '/../templates/token.html.php', [ 18 | 'message' => 'Missing token', 19 | ]), 401); 20 | } 21 | if (! hash_equals(Configuration::getConfig('authentication', 'token'), $request->attribute('token'))) { 22 | return new Response(render(__DIR__ . '/../templates/token.html.php', [ 23 | 'message' => 'Invalid token', 24 | ]), 401); 25 | } 26 | 27 | return $next($request); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /bridges/TheRedHandFilesBridge.php: -------------------------------------------------------------------------------- 1 | getURI()); 14 | 15 | foreach ($html->find('#main article.posts__article') as $element) { 16 | $item = []; 17 | 18 | $html_title = $element->find('h2', 0); 19 | $html_subtitle = $element->find('h3', 0); 20 | $html_image = $element->find('.posts__article-img', 0); 21 | 22 | $item['title'] = $html_subtitle->plaintext; 23 | $item['uri'] = $html_title->find('a', 0)->href; 24 | $item['content'] = $html_image->innertext . $html_title->plaintext; 25 | 26 | $this->items[] = $item; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /bridges/BleepingComputerBridge.php: -------------------------------------------------------------------------------- 1 | collectExpandableDatas($feed); 14 | } 15 | 16 | protected function parseItem(array $item) 17 | { 18 | $article_html = getSimpleHTMLDOMCached($item['uri']); 19 | if (!$article_html) { 20 | $item['content'] .= '

Could not request ' . $this->getName() . ': ' . $item['uri'] . '

'; 21 | return $item; 22 | } 23 | 24 | $article_content = $article_html->find('div.articleBody', 0)->innertext; 25 | $article_content = stripRecursiveHTMLSection($article_content, 'div', '
getURI(), '/') . $element->find('.shm-thumb-link', 0)->href; 16 | $item['id'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE)); 17 | $item['timestamp'] = time(); 18 | $thumbnailUri = $element->find('a', 1)->href; 19 | $item['categories'] = explode(' ', $element->getAttribute('data-tags')); 20 | $item['title'] = $this->getName() . ' | ' . $item['id']; 21 | $item['content'] = '
Tags: ' 26 | . $element->getAttribute('data-tags'); 27 | return $item; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /bridges/AcrimedBridge.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'name' => 'limit', 15 | 'type' => 'number', 16 | 'defaultValue' => -1, 17 | ] 18 | ] 19 | ]; 20 | 21 | public function collectData() 22 | { 23 | $url = 'https://www.acrimed.org/spip.php?page=backend'; 24 | $limit = $this->getInput('limit'); 25 | $this->collectExpandableDatas($url, $limit); 26 | } 27 | 28 | protected function parseItem(array $item) 29 | { 30 | $articlePage = getSimpleHTMLDOM($item['uri']); 31 | $article = sanitize($articlePage->find('article.article1', 0)->innertext); 32 | $article = defaultLinkTo($article, static::URI); 33 | $item['content'] = $article; 34 | 35 | return $item; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /bridges/EngadgetBridge.php: -------------------------------------------------------------------------------- 1 | collectExpandableDatas($url, $max); 16 | } 17 | 18 | protected function parseItem(array $item) 19 | { 20 | $itemUrl = trim($item['uri']); 21 | if (!$itemUrl) { 22 | return $item; 23 | } 24 | // todo: remove querystring tracking 25 | $dom = getSimpleHTMLDOM($itemUrl); 26 | // figure contain's the main article image 27 | $article = $dom->find('figure', 0); 28 | // .article-text has the actual article 29 | foreach ($dom->find('.article-text') as $element) { 30 | $article = $article . $element; 31 | } 32 | $item['content'] = $article ?? ''; 33 | return $item; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /bridges/QwerteeBridge.php: -------------------------------------------------------------------------------- 1 | find('div.big-slides', 0)->find('div.big-slide') as $element) { 18 | $title = $element->find('div.index-tee', 0)->getAttribute('data-name', 0); 19 | $today = date('m/d/Y'); 20 | $item = []; 21 | $item['uri'] = self::URI; 22 | $item['title'] = $title; 23 | $item['uid'] = $title; 24 | $item['timestamp'] = $today; 25 | $item['content'] = ''; 30 | 31 | $this->items[] = $item; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /bridges/BastaBridge.php: -------------------------------------------------------------------------------- 1 | find('item') as $element) { 18 | if ($limit < 10) { 19 | $item = []; 20 | $item['title'] = $element->find('title', 0)->innertext; 21 | $item['uri'] = $element->find('guid', 0)->plaintext; 22 | $item['timestamp'] = strtotime($element->find('dc:date', 0)->plaintext); 23 | 24 | $html = getSimpleHTMLDOM($item['uri']); 25 | $html = defaultLinkTo($html, self::URI); 26 | 27 | $item['content'] = $html->find('div.texte', 0)->innertext; 28 | $this->items[] = $item; 29 | $limit++; 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /bridges/LogicMastersBridge.php: -------------------------------------------------------------------------------- 1 | parse($value); 25 | } 26 | } -------------------------------------------------------------------------------- /lib/php-urljoin/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 j. shagam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/parsedown/LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2018 Emanuil Rusev, erusev.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/simplehtmldom/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 S.C. Chen, John Schlick, logmanoriginal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /bridges/NiceMatinBridge.php: -------------------------------------------------------------------------------- 1 | collectExpandableDatas(self::URI . 'derniere-minute/rss', 10); 13 | } 14 | 15 | protected function parseItem(array $item) 16 | { 17 | $item['content'] = $this->extractContent($item['uri']); 18 | return $item; 19 | } 20 | 21 | private function extractContent($url) 22 | { 23 | $html = getSimpleHTMLDOMCached($url); 24 | if (!$html) { 25 | return 'Could not acquire content from url: ' . $url . '!'; 26 | } 27 | 28 | $content = $html->find('article', 0); 29 | if (!$content) { 30 | return 'Could not find \'section\'!'; 31 | } 32 | 33 | $text = preg_replace('#(.*?)#is', '', $content->innertext); 34 | $text = strip_tags($text, '

'); 35 | return $text; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rss-bridge dev", 3 | "build": { "dockerfile": "Dockerfile" }, 4 | "customizations": { 5 | // Configure properties specific to VS Code. 6 | "vscode": { 7 | // Set *default* container specific settings.json values on container create. 8 | "settings": { 9 | "php.validate.executablePath": "/usr/local/bin/php", 10 | "phpSniffer.executablesFolder": "/usr/local/bin/", 11 | "phpcs.executablePath": "/usr/local/bin/phpcs", 12 | "phpcs.lintOnType": false 13 | }, 14 | 15 | // Add the IDs of extensions you want installed when the container is created. 16 | "extensions": [ 17 | "xdebug.php-debug", 18 | "bmewburn.vscode-intelephense-client", 19 | "philfontaine.autolaunch", 20 | "eamodio.gitlens", 21 | "shevaua.phpcs" 22 | ] 23 | } 24 | }, 25 | "forwardPorts": [3100, 9000, 9003], 26 | "postCreateCommand": "cp .devcontainer/nginx.conf /etc/nginx/conf.d/default.conf && cp .devcontainer/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini && mkdir .vscode && cp .devcontainer/launch.json .vscode && echo '*' > whitelist.txt && chmod a+x \"$(pwd)\" && rm -rf /var/www/html && ln -s \"$(pwd)\" /var/www/html && nginx && php-fpm -D" 27 | } -------------------------------------------------------------------------------- /bridges/FabriceBellardBridge.php: -------------------------------------------------------------------------------- 1 | find('p') as $obj) { 15 | $item = []; 16 | 17 | $html = defaultLinkTo($html, $this->getURI()); 18 | 19 | $links = $obj->find('a'); 20 | if (count($links) > 0) { 21 | $link_uri = $links[0]->href; 22 | } else { 23 | $link_uri = $this->getURI(); 24 | } 25 | 26 | /* try to make sure the link is valid */ 27 | if ($link_uri[-1] !== '/' && strpos($link_uri, '/') === false) { 28 | $link_uri = $link_uri . '/'; 29 | } 30 | 31 | $item['title'] = strip_tags($obj->innertext); 32 | $item['uri'] = $link_uri; 33 | $item['content'] = $obj->innertext; 34 | 35 | $this->items[] = $item; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /bridges/QwantzBridge.php: -------------------------------------------------------------------------------- 1 | collectExpandableDatas(self::URI . 'rssfeed.php'); 12 | } 13 | 14 | protected function parseItem(array $item) 15 | { 16 | $item['author'] = 'Ryan North'; 17 | 18 | preg_match('/title="(.*?)"/', $item['content'], $matches); 19 | $title = $matches[1] ?? ''; 20 | 21 | $content = str_get_html(html_entity_decode($item['content'])); 22 | $comicURL = $content->find('img')[0]->{'src'}; 23 | $subject = $content->find('a')[1]->{'href'}; 24 | $subject = urldecode(substr($subject, strpos($subject, 'subject') + 8)); 25 | $p = (string)$content->find('P')[0]; 26 | 27 | $item['content'] = "{$subject}

{$title}

{$p}"; 28 | 29 | return $item; 30 | } 31 | 32 | public function getIcon() 33 | { 34 | return self::URI . 'favicon.ico'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /docs/10_Bridge_Specific/Substack.md: -------------------------------------------------------------------------------- 1 | # SubstackBridge 2 | 3 | [Substack](https://substack.com) provides RSS feeds at `/feed` path, e.g., https://newsletter.pragmaticengineer.com/feed/. However, these feeds have two problems, addressed by this bridge: 4 | - They use RSS 2.0 with the draft [content extension](https://web.resource.org/rss/1.0/modules/content/), which isn't supported by some readers; 5 | - They don't have the full content for paywalled posts. 6 | 7 | Retrieving the full content is only possible _with an active subscription to the blog_. If you have one, Substack will return the full feed if it's fetched with the right set of cookies. Figuring out whether it's the intended behaviour is left as an exercise for the reader. 8 | 9 | To obtain the session cookie, authorize at https://substack.com/, open DevTools, go to Application -> Cookies -> https://substack.com, copy the value of `substack.sid` and paste it to the RSS bridge config: 10 | 11 | ``` 12 | [SubstackBridge] 13 | sid = "" 14 | ``` 15 | 16 | Authorization sometimes requires CAPTCHA, hence this operation is manual. The cookie lives for three months. 17 | 18 | After you've done this, the bridge should return full feeds for your subscriptions. 19 | -------------------------------------------------------------------------------- /bridges/UsesTechBridge.php: -------------------------------------------------------------------------------- 1 | find('div[class=PersonInner]') as $index => $a) { 17 | $item = []; // Create an empty item 18 | $articlePath = $a->find('a[class=displayLink]', 0)->href; 19 | $item['title'] = $a->find('img', 0)->getAttribute('alt'); 20 | $item['author'] = $a->find('img', 0)->getAttribute('alt'); 21 | $item['uri'] = $articlePath; 22 | $item['content'] = $a->find('p', 0)->innertext; 23 | 24 | $this->items[] = $item; // Add item to the list 25 | if (count($this->items) >= self::MAX_ITEM) { 26 | break; 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /docs/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "RSS-Bridge", 3 | "tagline": "The RSS feed for websites missing it", 4 | "author": "RSS-Bridge Contributors", 5 | "image": "./images/rssbridgelogo.png", 6 | "ignore": { 7 | "files": ["Work_In_Progress.md", "readme.md"], 8 | "folders": ["99_Theme"] 9 | }, 10 | "live": { 11 | "clean_urls": true 12 | }, 13 | "templates": "daux/templates", 14 | "html": { 15 | "theme": "daux-blue", 16 | "breadcrumbs": true, 17 | "breadcrumb_separator": "Chevrons", 18 | "toggle_code": true, 19 | "date_modified": true, 20 | "inherit_index": true, 21 | 22 | "repo": "RSS-Bridge/rss-bridge", 23 | "edit_on_github": "RSS-Bridge/rss-bridge/tree/master/docs", 24 | "google_analytics": false, 25 | "plausible_domain": false, 26 | "links": { 27 | "GitHub Repository": "https://github.com/RSS-Bridge/rss-bridge", 28 | "Help/Support/Bugs": "https://github.com/RSS-Bridge/rss-bridge/issues", 29 | "Docker Images": "https://github.com/RSS-Bridge/rss-bridge/pkgs/container/rss-bridge" 30 | }, 31 | "powered_by": "Powered by Daux.io" 32 | } 33 | } -------------------------------------------------------------------------------- /bridges/MondeDiploBridge.php: -------------------------------------------------------------------------------- 1 | find('div.unarticle') as $article) { 21 | $element = $article->parent(); 22 | $title = $element->find('h3', 0)->plaintext; 23 | $datesAuteurs = $element->find('div.dates_auteurs', 0)->plaintext; 24 | $item = []; 25 | $item['uri'] = urljoin(self::URI, $element->href); 26 | $item['title'] = $this->cleanText($title) . ' - ' . $this->cleanText($datesAuteurs); 27 | $item['content'] = $this->cleanText(str_replace([$title, $datesAuteurs], '', $element->plaintext)); 28 | 29 | $this->items[] = $item; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /bridges/BinanceBridge.php: -------------------------------------------------------------------------------- 1 | data->blogList as $post) { 17 | $item = []; 18 | $item['title'] = $post->title; 19 | // Url slug not in json 20 | //$item['uri'] = $uri; 21 | $item['timestamp'] = $post->postTimeUTC / 1000; 22 | $item['author'] = 'Binance'; 23 | $item['content'] = $post->brief; 24 | //$item['categories'] = $category; 25 | $item['uid'] = $post->idStr; 26 | $this->items[] = $item; 27 | } 28 | } 29 | 30 | public function getIcon() 31 | { 32 | return 'https://bin.bnbstatic.com/static/images/common/favicon.ico'; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /bridges/PCGWNewsBridge.php: -------------------------------------------------------------------------------- 1 | getURI()); 19 | 20 | $now = strtotime('now'); 21 | 22 | foreach ($html->find('.mw-parser-output .news_li') as $element) { 23 | $item = []; 24 | 25 | $date_string = $element->find('b', 0)->innertext; 26 | $date = strtotime($date_string); 27 | if ($date > $now) { 28 | $date = strtotime($date_string . ' - 1 year'); 29 | } 30 | $item['title'] = self::NAME . ' for ' . date('Y-m-d', $date); 31 | $item['content'] = $element; 32 | $item['uri'] = $this->getURI(); 33 | $item['timestamp'] = $date; 34 | 35 | $this->items[] = $item; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /bridges/LuftsportSHBridge.php: -------------------------------------------------------------------------------- 1 | getTimestamp(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /bridges/MozillaSecurityBridge.php: -------------------------------------------------------------------------------- 1 | find('div[id="main-content"] h2'); 20 | 21 | foreach ($articles as $element) { 22 | //Limit total amount of requests 23 | if (count($this->items) >= 20) { 24 | break; 25 | } 26 | $item['title'] = $element->innertext; 27 | $item['timestamp'] = strtotime($element->innertext); 28 | $item['content'] = $element->next_sibling()->innertext; 29 | $item['uri'] = self::URI . '?' . $item['timestamp']; 30 | $item['uid'] = self::URI . '?' . $item['timestamp']; 31 | $this->items[] = $item; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /bridges/BundesverbandFuerFreieKammernBridge.php: -------------------------------------------------------------------------------- 1 | setTime(0, 0, 0); 26 | return $dti->getTimestamp(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /bridges/DeutscherAeroClubBridge.php: -------------------------------------------------------------------------------- 1 | setTime(0, 0, 0); 25 | return $dti->getTimestamp(); 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /bridges/N26Bridge.php: -------------------------------------------------------------------------------- 1 | find('div[class="bl bm"]'); 18 | 19 | foreach ($articles as $article) { 20 | $item = []; 21 | 22 | $itemUrl = self::URI . $article->find('a', 1)->href; 23 | $item['uri'] = $itemUrl; 24 | 25 | $item['title'] = $article->find('a', 1)->plaintext; 26 | 27 | $fullArticle = getSimpleHTMLDOM($item['uri']); 28 | 29 | $createdAt = $fullArticle->find('time', 0); 30 | $item['timestamp'] = strtotime($createdAt->plaintext); 31 | 32 | $this->items[] = $item; 33 | if (count($this->items) >= $limit) { 34 | break; 35 | } 36 | } 37 | } 38 | 39 | public function getIcon() 40 | { 41 | return 'https://n26.com/favicon.ico'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | 26 | -------------------------------------------------------------------------------- /bridges/HanimeBridge.php: -------------------------------------------------------------------------------- 1 | find('.htv-carousel__scrolls', 0); 13 | $html = defaultLinkTo($html, $this->getURI()); 14 | 15 | foreach ($html->find('.item') as $video) { 16 | $item = []; 17 | 18 | $video_uri = $video->find('.no-touch', 0)->href; 19 | 20 | // Get video cover url 21 | // Use regex to get video_uri title 22 | $exp = '/\/([A-Za-z\-0-9]+$)/m'; 23 | preg_match_all($exp, $video_uri, $matches, PREG_SET_ORDER, 0); 24 | // Use the video title as name for the cover file 25 | $cover = 'https://cdn.statically.io/img/akidoo.top/images/covers/' . $matches[0][1] . '-cv1.png'; 26 | 27 | $item['uri'] = $video_uri; 28 | $item['title'] = $video->find('.hv-title', 0)->plaintext; 29 | $item['content'] = sprintf('', $cover); 30 | 31 | $this->items[] = $item; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /bridges/LesJoiesDuCodeBridge.php: -------------------------------------------------------------------------------- 1 | find('article.blog-post') as $element) { 16 | $item = []; 17 | $temp = $element->find('h1 a', 0); 18 | $titre = html_entity_decode($temp->innertext); 19 | $url = $temp->href; 20 | 21 | $temp = $element->find('div.blog-post-content', 0); 22 | 23 | // retrieve .gif instead of static .jpg 24 | $images = $temp->find('p img'); 25 | foreach ($images as $image) { 26 | $img_src = str_replace('.jpg', '.gif', $image->src); 27 | $image->src = $img_src; 28 | } 29 | $content = $temp->innertext; 30 | 31 | $item['content'] = trim($content); 32 | $item['uri'] = $url; 33 | $item['title'] = trim($titre); 34 | 35 | $this->items[] = $item; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /templates/connectivity.html.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |
19 | 26 | 27 |
28 | 29 | -------------------------------------------------------------------------------- /docs/05_Bridge_API/01_How_to_create_a_new_bridge.md: -------------------------------------------------------------------------------- 1 | Create a new file in the `bridges/` folder (see [Folder structure](../04_For_Developers/03_Folder_structure.md)). 2 | 3 | The file name must be named according to following specification: 4 | * It starts with the full name of the site 5 | * All white-space must be removed 6 | * The first letter of a word is written in upper-case, unless the site name is specified otherwise (example: Freenews, not FreeNews, because the site is named 'Freenews') 7 | * The first character must be upper-case 8 | * The file name must end with 'Bridge' 9 | * The file type must be PHP, written in **small** letters (seriously!) ".php" 10 | 11 | **Examples:** 12 | 13 | Site | Filename 14 | -----|--------- 15 | Wikipedia | **Wikipedia**Bridge.php 16 | Facebook | **Facebook**Bridge.php 17 | GitHub | **GitHub**Bridge.php 18 | Freenews | **Freenews**Bridge.php 19 | 20 | The file must start with the PHP tags and end with an empty line. The closing tag `?>` is [omitted](http://php.net/basic-syntax.instruction-separation). 21 | 22 | **Example:** 23 | 24 | ```php 25 | find('.main-center', 0); 17 | $elements = $mainContent->find('.row-fluid'); 18 | foreach ($elements as $i => $element) { 19 | if ($i === 0) { 20 | continue; 21 | } 22 | 23 | $titleEl = $element->find('.h3', 0); 24 | $title = is_object($titleEl) ? $titleEl->plaintext : ''; 25 | 26 | $postUrl = self::URI . $title; 27 | 28 | $contentEl = $element->find('.span9', 0); 29 | $content = is_object($contentEl) ? $contentEl->xmltext() : ''; 30 | 31 | $item = []; 32 | $item['uri'] = $postUrl; 33 | $item['title'] = $title; 34 | $item['content'] = $content; 35 | $item['timestamp'] = strtotime('now'); 36 | 37 | $this->items[] = $item; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /bridges/Shimmie2Bridge.php: -------------------------------------------------------------------------------- 1 | getURI() 15 | . 'post/list/' 16 | . $this->getInput('t') 17 | . '/' 18 | . $this->getInput('p'); 19 | } 20 | 21 | protected function getItemFromElement($element) 22 | { 23 | $item = []; 24 | $item['uri'] = $this->getURI() . $element->href; 25 | $item['id'] = (int)preg_replace('/[^0-9]/', '', $element->getAttribute(static::IDATTRIBUTE)); 26 | $item['timestamp'] = time(); 27 | $thumbnailUri = $this->getURI() . $element->find('img', 0)->src; 28 | $item['categories'] = explode(' ', $element->getAttribute('data-tags')); 29 | $item['title'] = $this->getName() . ' | ' . $item['id']; 30 | $item['content'] = '

Tags: ' 35 | . $element->getAttribute('data-tags'); 36 | 37 | return $item; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /bridges/TheYeteeBridge.php: -------------------------------------------------------------------------------- 1 | find('.module_timed-item.is--full'); 16 | foreach ($div as $element) { 17 | $item = []; 18 | $item['enclosures'] = []; 19 | 20 | $title = $element->find('h2', 0)->plaintext; 21 | $item['title'] = $title; 22 | 23 | $author = trim($element->find('.module_timed-item--artist a', 0)->plaintext); 24 | $item['author'] = $author; 25 | 26 | $item['uri'] = static::URI; 27 | 28 | $content = '

' . $title . ' by ' . $author . '

'; 29 | $photos = $element->find('a.img'); 30 | foreach ($photos as $photo) { 31 | $content = $content . "
"; 32 | $item['enclosures'][] = $photo->src; 33 | } 34 | $item['content'] = $content; 35 | 36 | $this->items[] = $item; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /bridges/ExecuteProgramBridge.php: -------------------------------------------------------------------------------- 1 | posts as $post) { 15 | $year = $post->date->year; 16 | $month = $post->date->month; 17 | $day = $post->date->day; 18 | 19 | $item = []; 20 | $item['uri'] = sprintf('https://www.executeprogram.com/blog/%s', $post->slug); 21 | $item['title'] = $post->title; 22 | $dateTime = \DateTime::createFromFormat('Y-m-d', $year . '-' . $month . '-' . $day); 23 | $item['timestamp'] = $dateTime->format('U'); 24 | $item['content'] = $post->body; 25 | 26 | $this->items[] = $item; 27 | } 28 | 29 | usort($this->items, function ($a, $b) { 30 | return $a['timestamp'] < $b['timestamp']; 31 | }); 32 | } 33 | 34 | public function getIcon() 35 | { 36 | return 'https://www.executeprogram.com/favicon.ico'; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/RssBridge.php: -------------------------------------------------------------------------------- 1 | container = $container; 11 | } 12 | 13 | public function main(Request $request): Response 14 | { 15 | $action = $request->get('action', 'Frontpage'); 16 | $actionName = strtolower($action) . 'Action'; 17 | $actionName = implode(array_map('ucfirst', explode('-', $actionName))); 18 | $filePath = __DIR__ . '/../actions/' . $actionName . '.php'; 19 | if (!file_exists($filePath)) { 20 | return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'Invalid action']), 400); 21 | } 22 | 23 | $handler = $this->container[$actionName]; 24 | 25 | $middlewares = [ 26 | new SecurityMiddleware(), 27 | new MaintenanceMiddleware(), 28 | new BasicAuthMiddleware(), 29 | new TokenAuthenticationMiddleware(), 30 | ]; 31 | $action = function ($req) use ($handler) { 32 | return $handler($req); 33 | }; 34 | foreach (array_reverse($middlewares) as $middleware) { 35 | $action = fn ($req) => $middleware($req, $action); 36 | } 37 | return $action($request); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /bridges/OpenwrtSecurityBridge.php: -------------------------------------------------------------------------------- 1 | find('div[class=plugin_nspages]', 0); 18 | 19 | foreach ($advisories->find('a[class=wikilink1]') as $element) { 20 | $item = []; 21 | 22 | $row = $element->innertext; 23 | 24 | $item['title'] = substr($row, 0, strpos($row, ' - ')); 25 | $item['timestamp'] = $this->getDate($element->href); 26 | $item['uri'] = self::WEBROOT . $element->href; 27 | $item['uid'] = self::WEBROOT . $element->href; 28 | $item['content'] = substr($row, strpos($row, ' - ') + 3); 29 | $item['author'] = 'OpenWrt Project'; 30 | 31 | $this->items[] = $item; 32 | } 33 | } 34 | 35 | private function getDate($href) 36 | { 37 | $date = substr($href, -12); 38 | return $date; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /bridges/QnapBridge.php: -------------------------------------------------------------------------------- 1 | Use offical feed instead: https://www.qnap.com/fr-fr/security-news/feed

11 | Unofficial feed for security news. 12 | DESCRIPTION; 13 | 14 | const MAINTAINER = 'dvikan'; 15 | 16 | public function collectData() 17 | { 18 | $thisYear = date('Y'); 19 | $url = sprintf('https://www.qnap.com/api/v1/articles/security-news?locale=fr-fr&year=%s&page=1', $thisYear); 20 | $response = json_decode(getContents($url)); 21 | foreach ($response->data as $post) { 22 | $item = []; 23 | $item['uri'] = sprintf('https://www.qnap.com%s', $post->url); 24 | $item['title'] = $post->title; 25 | $item['timestamp'] = \DateTime::createFromFormat('Y-m-d', $post->date)->format('U'); 26 | $image = sprintf('', $post->image_url); 27 | $item['content'] = $image . '

' . $post->desc; 28 | $this->items[] = $item; 29 | } 30 | usort($this->items, function ($a, $b) { 31 | return $a['timestamp'] < $b['timestamp']; 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /bridges/CopieDoubleBridge.php: -------------------------------------------------------------------------------- 1 | find('table table', 2); 16 | 17 | foreach ($table->find('tr') as $element) { 18 | $td = $element->find('td', 0); 19 | 20 | if ($td->class === 'couleur_1') { 21 | $item = []; 22 | $title = $td->innertext; 23 | $pos = strpos($title, 'innertext, '/images/suivant.gif') === false) { 27 | $a = $element->find('a', 0); 28 | $item['uri'] = self::URI . $a->href; 29 | $content = str_replace('src="/', 'src="/' . self::URI, $element->find('td', 0)->innertext); 30 | $content = str_replace('href="/', 'href="' . self::URI, $content); 31 | $item['content'] = $content; 32 | $this->items[] = $item; 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /bridges/TeefuryBridge.php: -------------------------------------------------------------------------------- 1 | find('div.odad-card__wrapper') as $element) { 19 | $titletext = $element->find('p', 0)->innertext; 20 | $title = trim(explode('
', $titletext)[0]); 21 | $today = date('m/d/Y'); 22 | $shirtinfo = $element->find('div[data-link*="odad-tee-mens"]', 0); 23 | $uri = self::URI . $shirtinfo->attr['data-link']; 24 | $item = []; 25 | $item['uri'] = $uri; 26 | $item['title'] = $title; 27 | $item['uid'] = $title; 28 | $item['timestamp'] = $today; 29 | $item['content'] = $element->find('p', 0) 30 | . '
'; 35 | 36 | $this->items[] = $item; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /bridges/CryptomeBridge.php: -------------------------------------------------------------------------------- 1 | [ 12 | 'name' => 'number of elements', 13 | 'type' => 'number', 14 | 'required' => true, 15 | 'exampleValue' => 10 16 | ] 17 | ]]; 18 | 19 | public function getIcon() 20 | { 21 | return self::URI . '/favicon.ico'; 22 | } 23 | 24 | public function collectData() 25 | { 26 | $html = getSimpleHTMLDOM(self::URI); 27 | 28 | $number = $this->getInput('n'); 29 | if (!empty($number)) { 30 | $num = min($number, 20); 31 | } 32 | $i = 0; 33 | foreach ($html->find('pre', 1)->find('b') as $element) { 34 | foreach ($element->find('a') as $element1) { 35 | $item = []; 36 | $item['uri'] = $element1->href; 37 | $item['title'] = $element->plaintext; 38 | $this->items[] = $item; 39 | 40 | if ($i > $num) { 41 | break 2; 42 | } 43 | $i++; 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/FormatAbstract.php: -------------------------------------------------------------------------------- 1 | '', 20 | 'uri' => '', 21 | 'icon' => '', 22 | 'donationUri' => '', 23 | ]; 24 | $this->feed = array_merge($default, $feed); 25 | } 26 | 27 | public function getFeed(): array 28 | { 29 | return $this->feed; 30 | } 31 | 32 | public function setItems(array $items): void 33 | { 34 | foreach ($items as $item) { 35 | $this->items[] = FeedItem::fromArray($item); 36 | } 37 | } 38 | 39 | /** 40 | * @return FeedItem[] The items 41 | */ 42 | public function getItems(): array 43 | { 44 | return $this->items; 45 | } 46 | 47 | public function getMimeType(): string 48 | { 49 | return static::MIME_TYPE; 50 | } 51 | 52 | public function setLastModified(int $lastModified) 53 | { 54 | $this->lastModified = $lastModified; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /bridges/ScoopItBridge.php: -------------------------------------------------------------------------------- 1 | [ 13 | 'name' => 'keyword', 14 | 'exampleValue' => 'docker', 15 | 'required' => true 16 | ] 17 | ]]; 18 | 19 | public function collectData() 20 | { 21 | $this->request = $this->getInput('u'); 22 | $link = self::URI . 'search?q=' . urlencode($this->getInput('u')); 23 | 24 | $html = getSimpleHTMLDOM($link); 25 | 26 | foreach ($html->find('div.post-view') as $element) { 27 | $item = []; 28 | $item['uri'] = $element->find('a', 0)->href; 29 | $item['title'] = preg_replace( 30 | '~[[:cntrl:]]~', 31 | '', 32 | $element->find('div.tCustomization_post_title', 0)->plaintext 33 | ); 34 | 35 | $item['content'] = preg_replace( 36 | '~[[:cntrl:]]~', 37 | '', 38 | $element->find('div.tCustomization_post_description', 0)->plaintext 39 | ); 40 | 41 | $this->items[] = $item; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /bridges/GoAccessBridge.php: -------------------------------------------------------------------------------- 1 | find('.container.content', 0); 17 | foreach ($container->find('div') as $element) { 18 | $titleEl = $element->find('h2', 0); 19 | $dateEl = $titleEl->find('small', 0); 20 | $date = trim($dateEl->plaintext); 21 | $title = is_object($titleEl) ? str_replace($date, '', $titleEl->plaintext) : ''; 22 | $linkEl = $titleEl->find('a', 0); 23 | $link = is_object($linkEl) ? $linkEl->href : ''; 24 | $postUrl = self::URI . $link; 25 | 26 | $contentEl = $element->find('.dl-horizontal', 0); 27 | $content = '
' . $contentEl->xmltext() . '
'; 28 | 29 | $item = []; 30 | $item['uri'] = $postUrl; 31 | $item['timestamp'] = strtotime($date); 32 | $item['title'] = $title; 33 | $item['content'] = $content; 34 | 35 | $this->items[] = $item; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /actions/ListAction.php: -------------------------------------------------------------------------------- 1 | bridgeFactory = $bridgeFactory; 11 | } 12 | 13 | public function __invoke(Request $request): Response 14 | { 15 | $list = new \stdClass(); 16 | $list->bridges = []; 17 | $list->total = 0; 18 | 19 | foreach ($this->bridgeFactory->getBridgeClassNames() as $bridgeClassName) { 20 | $bridge = $this->bridgeFactory->create($bridgeClassName); 21 | 22 | $list->bridges[$bridgeClassName] = [ 23 | 'status' => $this->bridgeFactory->isEnabled($bridgeClassName) ? 'active' : 'inactive', 24 | 'uri' => $bridge->getURI(), 25 | 'donationUri' => $bridge->getDonationURI(), 26 | 'name' => $bridge->getName(), 27 | 'icon' => $bridge->getIcon(), 28 | 'parameters' => $bridge->getParameters(), 29 | 'maintainer' => $bridge->getMaintainer(), 30 | 'description' => $bridge->getDescription() 31 | ]; 32 | } 33 | $list->total = count($list->bridges); 34 | return new Response(Json::encode($list), 200, ['content-type' => 'application/json']); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /bridges/RiptApparelBridge.php: -------------------------------------------------------------------------------- 1 | find('div.daily-designs', 0)->find('div.collection') as $element) { 18 | $title = $element->find('div.design-info', 0)->find('div.title', 0)->innertext; 19 | $uri = self::URI . $element->find('div.design-info', 0)->find('a', 0)->href; 20 | $today = date('m/d/Y'); 21 | $imagesrcset = $element->find('div.design-images', 0)->find('div[data-subtype="Mens"]', 0)->find('img', 0); 22 | $image = rtrim(explode(',', $imagesrcset->getAttribute('data-srcset'))[2], ' 900w'); 23 | $item = []; 24 | $item['uri'] = $uri; 25 | $item['title'] = $title; 26 | $item['uid'] = $title; 27 | $item['timestamp'] = $today; 28 | $item['content'] = ''; 33 | 34 | $this->items[] = $item; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /bridges/SpottschauBridge.php: -------------------------------------------------------------------------------- 1 | find('div.strip>a', 0)->attr['href']); 19 | $item['title'] = $html->find('div.text>h2', 0)->innertext; 20 | 21 | $date = preg_replace('/.*, /', '', $item['title']); 22 | $date = preg_replace('/\\d\\d\\.\\//', '', $date); 23 | try { 24 | $item['timestamp'] = DateTime::createFromFormat('d.m.y', $date) 25 | ->setTimezone(new DateTimeZone('Europe/Berlin')) 26 | ->setTime(0, 0) 27 | ->getTimestamp(); 28 | } catch (Throwable $ignored) { 29 | $item['timestamp'] = null; 30 | } 31 | 32 | $image = $html->find('div.strip>a>img', 0); 33 | $imageUrl = urljoin(self::URI, $image->attr['src']); 34 | $imageAlt = $image->attr['alt']; 35 | 36 | $item['content'] = << 38 |
39 | EOD; 40 | $this->items[] = $item; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /bridges/ParksOnTheAirBridge.php: -------------------------------------------------------------------------------- 1 | 1]; 16 | $json = getContents(self::API_URI, $header, $opts); 17 | 18 | $spots = json_decode($json, true); 19 | 20 | foreach ($spots as $spot) { 21 | $title = $spot['activator'] . ' @ ' . $spot['reference'] . ' ' . 22 | $spot['frequency'] . ' kHz'; 23 | $park_link = self::URI . '/park/' . $spot['reference']; 24 | 25 | $content = << 27 | {$spot['reference']}, {$spot['name']}
28 | Location: {$spot['locationDesc']}
29 | Frequency: {$spot['frequency']} kHz
30 | Spotter: {$spot['spotter']}
31 | Comments: {$spot['comments']} 32 | EOL; 33 | 34 | $this->items[] = [ 35 | 'uri' => $park_link, 36 | 'title' => $title, 37 | 'content' => $content, 38 | 'timestamp' => $spot['spotTime'] 39 | ]; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /caches/ArrayCache.php: -------------------------------------------------------------------------------- 1 | data[$key] ?? null; 15 | if (!$item) { 16 | return $default; 17 | } 18 | $expiration = $item['expiration']; 19 | if ($expiration === 0 || $expiration > time()) { 20 | return $item['value']; 21 | } 22 | $this->delete($key); 23 | return $default; 24 | } 25 | 26 | public function set(string $key, $value, int $ttl = null): void 27 | { 28 | $this->data[$key] = [ 29 | 'key' => $key, 30 | 'value' => $value, 31 | 'expiration' => $ttl === null ? 0 : time() + $ttl, 32 | ]; 33 | } 34 | 35 | public function delete(string $key): void 36 | { 37 | unset($this->data[$key]); 38 | } 39 | 40 | public function clear(): void 41 | { 42 | $this->data = []; 43 | } 44 | 45 | public function prune(): void 46 | { 47 | foreach ($this->data as $key => $item) { 48 | $expiration = $item['expiration']; 49 | if ($expiration === 0 || $expiration > time()) { 50 | continue; 51 | } 52 | $this->delete($key); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /bridges/PicartoBridge.php: -------------------------------------------------------------------------------- 1 | [ 11 | 'name' => 'Channel name', 12 | 'type' => 'text', 13 | 'required' => true, 14 | 'title' => 'Channel name', 15 | 'exampleValue' => 'Wysdrem', 16 | ], 17 | ] 18 | ]; 19 | 20 | public function collectData() 21 | { 22 | $channel = $this->getInput('channel'); 23 | $data = json_decode(getContents('https://api.picarto.tv/api/v1/channel/name/' . $channel), true); 24 | if (!$data['online']) { 25 | return; 26 | } 27 | $lastLive = new \DateTime($data['last_live']); 28 | $this->items[] = [ 29 | 'uri' => 'https://picarto.tv/' . $channel, 30 | 'title' => $data['name'] . ' is now online', 31 | 'content' => sprintf('', $data['thumbnails']['tablet']), 32 | 'timestamp' => $lastLive->getTimestamp(), 33 | 'uid' => 'https://picarto.tv/' . $channel . $lastLive->getTimestamp(), 34 | ]; 35 | } 36 | 37 | public function getName() 38 | { 39 | return 'Picarto - ' . $this->getInput('channel'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /bridges/MsnMondeBridge.php: -------------------------------------------------------------------------------- 1 | collectExpandableDatas(self::FEED_URL, 10); 26 | } 27 | 28 | protected function parseItem(array $item) 29 | { 30 | if (!preg_match('#fr-fr/actualite.*/ar-(?[\w]*)\?#', $item['uri'], $matches)) { 31 | return null; 32 | } 33 | 34 | $jsonString = getContents(self::JSON_URL . $matches['id']); 35 | $json = json_decode($jsonString, true); 36 | $item['content'] = $json['body']; 37 | if (!empty($json['authors'])) { 38 | $item['author'] = reset($json['authors'])['name']; 39 | } 40 | $item['timestamp'] = $json['createdDateTime']; 41 | foreach ($json['tags'] as $tag) { 42 | $item['categories'][] = $tag['label']; 43 | } 44 | return $item; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/bootstrap.php: -------------------------------------------------------------------------------- 1 | collectExpandableDatas('https://forgifs.com/gallery/srss/7'); 13 | } 14 | 15 | protected function parseItem(array $item) 16 | { 17 | $dom = str_get_html($item['content']); 18 | $img = $dom->find('img', 0); 19 | $poster = $img->src; 20 | 21 | // The actual gif is the same path but its id must be decremented by one. 22 | // Example: 23 | // http://forgifs.com/gallery/d/279419-2/Reporter-videobombed-shoulder-checks.gif 24 | // http://forgifs.com/gallery/d/279418-2/Reporter-videobombed-shoulder-checks.gif 25 | // Notice how this changes ----------^ 26 | // Now let's extract that number and do some math 27 | // Notice: Technically we could also load the content page but that would 28 | // require unnecessary traffic. As long as it works... 29 | $num = substr($img->src, 29, 6); 30 | $num -= 1; 31 | $img->src = substr_replace($img->src, $num, 29, strlen($num)); 32 | $img->width = 'auto'; 33 | $img->height = 'auto'; 34 | 35 | $item['content'] = (string) $dom; 36 | 37 | return $item; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /bridges/SymfonyCastsBridge.php: -------------------------------------------------------------------------------- 1 | find('div.user-notification-not-viewed'); 18 | 19 | foreach ($dives as $div) { 20 | $type = $div->find('h5', 0); 21 | $title = $div->find('a', 0); 22 | $dateString = $div->find('h5.font-gray', 0); 23 | $href = $div->find('a', 0); 24 | $hrefAttribute = $href->getAttribute('href'); 25 | $url = 'https://symfonycasts.com' . $hrefAttribute; 26 | 27 | $item = []; 28 | $item['uid'] = $div->getAttribute('data-mark-update-update-url-value'); 29 | $item['title'] = $title->innertext; 30 | 31 | // this natural language date string does not work 32 | $item['timestamp'] = $dateString->innertext; 33 | 34 | $item['content'] = $type->plaintext . '' . $title . ''; 35 | $item['uri'] = $url; 36 | $this->items[] = $item; // Add item to the list 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /bridges/ViceBridge.php: -------------------------------------------------------------------------------- 1 | [ 12 | 'name' => 'Feed', 13 | 'type' => 'list', 14 | 'values' => [ 15 | 'Vice News' => 'rss', 16 | 'Motherboard - Tech' => 'en_us/rss/topic/tech', 17 | 'Entertainment' => 'en_us/rss/topic/entertainment', 18 | 'Noisey - Music' => 'en_us/rss/topic/music', 19 | 'Munchies - Food' => 'en_us/rss/topic/food' 20 | ] 21 | ] 22 | ]]; 23 | 24 | public function collectData() 25 | { 26 | $feed = $this->getInput('feed'); 27 | if ($feed === 'rss') { 28 | // They changed url in Sep 2023 29 | $feed = 'en/rss'; 30 | } 31 | $feedURL = 'https://www.vice.com/' . $feed; 32 | $this->collectExpandableDatas($feedURL, 10); 33 | } 34 | 35 | protected function parseItem(array $item) 36 | { 37 | $articlePage = getSimpleHTMLDOM($item['uri']); 38 | // text and embedded content 39 | $article = $articlePage->find('.article__body', 0); 40 | $item['content'] = $article ?? ''; 41 | 42 | return $item; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /bridges/ViadeoCompanyBridge.php: -------------------------------------------------------------------------------- 1 | apple)'; 11 | 12 | const PARAMETERS = [ [ 13 | 'c' => [ 14 | 'name' => 'Company name', 15 | 'exampleValue' => 'apple', 16 | 'required' => true 17 | ] 18 | ]]; 19 | 20 | public function collectData() 21 | { 22 | // Redirects to https://emploi.lefigaro.fr/recherche/entreprises 23 | $url = sprintf('%sfr/company/%s', self::URI, $this->getInput('c')); 24 | 25 | $html = getSimpleHTMLDOM($url); 26 | 27 | // TODO: Fix broken xpath selector 28 | $elements = $html->find('//*[@id="company-newsfeed"]/ul/li'); 29 | 30 | foreach ($elements as $element) { 31 | $title = $element->find('p', 0)->innertext; 32 | if (!$title) { 33 | continue; 34 | } 35 | $item = []; 36 | $item['uri'] = $url; 37 | $item['title'] = mb_substr($element->find('p', 0)->innertext, 0, 100); 38 | $item['content'] = $element->find('p', 0)->innertext; 39 | ; 40 | $this->items[] = $item; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/10_Bridge_Specific/TwitterV2.md: -------------------------------------------------------------------------------- 1 | TwitterV2Bridge 2 | =============== 3 | 4 | To automatically retrieve Tweets containing potentially sensitive/age-restricted content, you'll need to acquire your own unique API Bearer token, which will be used by this Bridge to query Twitter's API v2. 5 | 6 | Configuration 7 | ------------- 8 | 9 | 1. Make a Twitter Developer account 10 | 11 | - Developer Portal: https://dev.twitter.com 12 | 13 | - I will not detail exactly how to do this, as the specific process will likely change over time. You should easily be able to find guides using your search engine of choice. 14 | 15 | - Note: as of April 2023, the "Free" access level no longer allows read access. The cheapest access level with read access is called "Basic". 16 | 17 | 2. Create a Twitter Project and App, get Bearer Token 18 | 19 | - Once you have an active Twitter Developer account, sign in to the dev portal 20 | 21 | - Create a new Project (name doesn't matter) 22 | 23 | - Create an App within the Project (again, name doesn't matter) 24 | 25 | - Go to the **Keys and tokens** tab 26 | 27 | - Generate a **Bearer Token** (you don't want the API Key and Secret, or the Access Token and Secret) 28 | 29 | 3. Configure RSS-Bridge 30 | 31 | - In **config.ini.php** (in rss-bridge root directory) add following lines at the end: 32 | 33 | ``` 34 | [TwitterV2Bridge] 35 | twitterv2apitoken = %Bearer Token from step 2% 36 | ``` 37 | - If you don't have a **config.ini.php**, create one by making a copy of **config.default.ini.php** 38 | -------------------------------------------------------------------------------- /bridges/CBCEditorsBlogBridge.php: -------------------------------------------------------------------------------- 1 | find('div.contentListCards', 0)->find('a[data-test=type-story]') as $element) { 16 | $headline = ($element->find('.headline', 0))->innertext; 17 | $timestamp = ($element->find('time', 0))->datetime; 18 | $articleUri = 'https://www.cbc.ca' . $element->href; 19 | $summary = ($element->find('div.description', 0))->innertext; 20 | $thumbnailUris = ($element->find('img[loading=lazy]', 0))->srcset; 21 | $thumbnailUri = rtrim(explode(',', $thumbnailUris)[0], ' 300w'); 22 | 23 | // Fill item 24 | $item = []; 25 | $item['uri'] = $articleUri; 26 | $item['id'] = $item['uri']; 27 | $item['timestamp'] = $timestamp; 28 | $item['title'] = $headline; 29 | $item['content'] = '
' . $summary; 31 | $item['author'] = 'Editor\'s Blog'; 32 | 33 | if (isset($item['title'])) { 34 | $this->items[] = $item; 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /bridges/GithubPullRequestBridge.php: -------------------------------------------------------------------------------- 1 | [ 10 | 'u' => [ 11 | 'name' => 'User name', 12 | 'exampleValue' => 'RSS-Bridge', 13 | 'required' => true 14 | ], 15 | 'p' => [ 16 | 'name' => 'Project name', 17 | 'exampleValue' => 'rss-bridge', 18 | 'required' => true 19 | ] 20 | ], 21 | 'Project Pull Requests' => [ 22 | 'c' => [ 23 | 'name' => 'Show Pull Request Comments', 24 | 'type' => 'checkbox' 25 | ], 26 | 'q' => [ 27 | 'name' => 'Search Query', 28 | 'defaultValue' => 'is:pr is:open sort:created-desc', 29 | 'required' => true 30 | ] 31 | ], 32 | 'Pull Request comments' => [ 33 | 'i' => [ 34 | 'name' => 'Pull Request number', 35 | 'type' => 'number', 36 | 'exampleValue' => '2100', 37 | 'required' => true 38 | ] 39 | ] 40 | ]; 41 | 42 | const BRIDGE_OPTIONS = [0 => 'Project Pull Requests', 1 => 'Pull Request comments']; 43 | const URL_PATH = 'pull'; 44 | const SEARCH_QUERY_PATH = 'pulls'; 45 | } 46 | -------------------------------------------------------------------------------- /bridges/BlaguesDeMerdeBridge.php: -------------------------------------------------------------------------------- 1 | find('div.blague') as $element) { 21 | $item = []; 22 | 23 | $item['uri'] = static::URI . '#' . $element->id; 24 | $item['author'] = $element->find('div[class="blague-footer"] p strong', 0)->plaintext; 25 | 26 | // Let the title be everything up to the first
27 | $item['title'] = trim(explode("\n", $element->find('div.text', 0)->plaintext)[0]); 28 | 29 | $item['content'] = strip_tags($element->find('div.text', 0)); 30 | 31 | // timestamp is part of: 32 | //

Par {author} le {date} dans {category}

33 | preg_match( 34 | '/.+le(.+)dans.*/', 35 | $element->find('div[class="blague-footer"]', 0)->plaintext, 36 | $matches 37 | ); 38 | 39 | $item['timestamp'] = strtotime($matches[1]); 40 | 41 | $this->items[] = $item; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /bridges/NikonDownloadCenterBridge.php: -------------------------------------------------------------------------------- 1 | getURI()); 25 | 26 | foreach ($html->find('dd>ul>li') as $element) { 27 | $date = $element->find('.date', 0)->plaintext; 28 | $productType = $element->find('.icon>img', 0)->alt; 29 | $desc = $element->find('p>a', 0)->plaintext; 30 | $link = urljoin(self::URI, $element->find('p>a', 0)->href); 31 | 32 | $item = [ 33 | 'title' => $desc, 34 | 'uri' => $link, 35 | 'timestamp' => strtotime($date), 36 | 'content' => << 38 | New/updated {$productType}:
39 | {$desc} 40 |

41 |

42 | {$date} 43 |

44 | EOD 45 | ]; 46 | $this->items[] = $item; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /bridges/EASeedBridge.php: -------------------------------------------------------------------------------- 1 | find('ea-grid', 0); 15 | if (!$dom) { 16 | throw new \Exception(sprintf('Unable to find css selector on `%s`', $url)); 17 | } 18 | $dom = defaultLinkTo($dom, $this->getURI()); 19 | foreach ($dom->find('ea-tile') as $article) { 20 | $a = $article->find('a', 0); 21 | $date = $article->find('div', 1)->plaintext; 22 | $title = $article->find('h3', 0)->plaintext; 23 | $author = $article->find('div', 0)->plaintext; 24 | 25 | $entry = getSimpleHTMLDOMCached($a->href, static::CACHE_TIMEOUT * 7 * 4); 26 | 27 | $content = $entry->find('main', 0); 28 | 29 | // remove header and links to other posts 30 | $content->find('ea-header', 0)->outertext = ''; 31 | $content->find('ea-section', -1)->outertext = ''; 32 | 33 | $this->items[] = [ 34 | 'title' => $title, 35 | 'author' => $author, 36 | 'uri' => $a->href, 37 | 'content' => $content, 38 | 'timestamp' => strtotime($date), 39 | ]; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /bridges/LaTeX3ProjectNewslettersBridge.php: -------------------------------------------------------------------------------- 1 | find('article tbody', 0); 16 | 17 | foreach ($newsContainer->find('tr') as $row) { 18 | $this->items[] = $this->collectArticle($row); 19 | } 20 | } 21 | 22 | private function collectArticle($element) 23 | { 24 | $item = []; 25 | $item['uri'] = static::URI . $element->find('td', 1)->find('a', 0)->href; 26 | $item['title'] = $element->find('td', 1)->find('a', 0)->plaintext; 27 | $item['timestamp'] = DateTime::createFromFormat('Y/m/d', $element->find('td', 0)->plaintext)->getTimestamp(); 28 | $item['content'] = $element->find('td', 2)->plaintext; 29 | $item['author'] = 'LaTeX3 Project'; 30 | return $item; 31 | } 32 | 33 | public function getIcon() 34 | { 35 | return self::URI . '/favicon.ico'; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /bridges/NYTBridge.php: -------------------------------------------------------------------------------- 1 | collectExpandableDatas($url, 40); 15 | } 16 | 17 | protected function parseItem(array $item) 18 | { 19 | $article = ''; 20 | 21 | try { 22 | $articlePage = getSimpleHTMLDOM($item['uri']); 23 | } catch (HttpException $e) { 24 | // 403 Forbidden, This means we got anti-bot response 25 | if ($e->getCode() === 403) { 26 | return $item; 27 | } 28 | throw $e; 29 | } 30 | // handle subtitle 31 | $subtitle = $articlePage->find('p.css-w6ymp8', 0); 32 | if ($subtitle != null) { 33 | $article .= '' . $subtitle->plaintext . ''; 34 | } 35 | 36 | // figure contain's the main article image 37 | $article .= $articlePage->find('figure', 0) . '
'; 38 | 39 | // section.meteredContent has the actual article 40 | foreach ($articlePage->find('section.meteredContent p') as $element) { 41 | $article .= '' . $element . ''; 42 | } 43 | 44 | $item['content'] = $article; 45 | return $item; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /bridges/Rue89Bridge.php: -------------------------------------------------------------------------------- 1 | items; 14 | foreach ($articles as $article) { 15 | $this->items[] = $this->getArticle($article); 16 | } 17 | } 18 | 19 | private function getArticle($articleInfo) 20 | { 21 | $articleJson = getContents($articleInfo->json_url); 22 | $article = json_decode($articleJson); 23 | $item = []; 24 | $item['title'] = $article->title; 25 | $item['uri'] = $article->url; 26 | if ($article->content_premium !== null) { 27 | $item['content'] = $article->content_premium; 28 | } else { 29 | $item['content'] = $article->content; 30 | } 31 | $item['timestamp'] = $article->date_publi; 32 | $item['author'] = $article->author->show_name; 33 | 34 | $item['enclosures'] = []; 35 | foreach ($article->images as $image) { 36 | $item['enclosures'][] = $image->url; 37 | } 38 | 39 | $item['categories'] = []; 40 | foreach ($article->categories as $category) { 41 | $item['categories'][] = $category->title; 42 | } 43 | 44 | return $item; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /docs/04_For_Developers/02_Pull_Request_policy.md: -------------------------------------------------------------------------------- 1 | Pull requests allow you to improve RSS-Bridge. Maintainers will have to understand your changes before merging. In order to make this process as efficient as possible, please follow the policies explained below. Maintainers will merge your pull request much faster that way. 2 | 3 | # Fix one issue per pull request 4 | 5 | It is considered good practice to fix one specific (or a specific set of) error(s). You can open multiple pull requests if you need to address multiple subjects. The same applies to adding features to RSS-Bridge. Maintainers must be able to comprehend your pull request for it to be merged quickly. 6 | 7 | # Respect the coding style policy 8 | 9 | The [coding style policy](./01_Coding_style_policy.md) requires you to write your code in certain ways. If you plan to get it merged into RSS-Bridge, please make sure your code follows the policy. Maintainers will only merge pull requests that pass all tests. 10 | 11 | # Properly name your commits 12 | 13 | Commits are not only for show, they do help maintainers understand what you did in your pull request, just like a table of contents in a well formed book (or Wiki). Here are a few rules you should follow: 14 | 15 | * When fixing a bridge (located in the `bridges` directory), write `[BridgeName] Feature`
(i.e. `[YoutubeBridge] Fix typo in video titles`). 16 | * When fixing other files, use `[FileName] Feature`
(i.e. `[index.php] Add multilingual support`). 17 | * When fixing a general problem that applies to multiple files, write `category: feature`
(i.e. `bridges: Fix various typos`). -------------------------------------------------------------------------------- /bridges/LeMondeInformatiqueBridge.php: -------------------------------------------------------------------------------- 1 | collectExpandableDatas(self::URI . 'rss/rss.xml', 10); 13 | } 14 | 15 | protected function parseItem(array $item) 16 | { 17 | $article_html = getSimpleHTMLDOMCached($item['uri']); 18 | 19 | //Deduce thumbnail URL from article image URL 20 | $item['enclosures'] = [ 21 | str_replace( 22 | '/grande/', 23 | '/petite/', 24 | $article_html->find('.article-image > img, figure > img', 0)->src 25 | ) 26 | ]; 27 | 28 | //No response header sets the encoding, explicit conversion is needed or subsequent xml_encode() will fail 29 | $content_node = $article_html->find('div.col-primary, div.col-sm-9', 0); 30 | $item['content'] = $this->cleanArticle($content_node->innertext); 31 | $item['author'] = $article_html->find('div.author-infos', 0)->find('b', 0)->plaintext; 32 | 33 | return $item; 34 | } 35 | 36 | private function cleanArticle($article_html) 37 | { 38 | $article_html = stripWithDelimiters($article_html, ''); 39 | $article_html = explode('

'Please authenticate in order to access this instance!', 33 | ]); 34 | return new Response($html, 401, ['WWW-Authenticate' => 'Basic realm="RSS-Bridge"']); 35 | } 36 | return $next($request); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docs/03_For_Hosts/04_Heroku_Installation.md: -------------------------------------------------------------------------------- 1 | This guide is for people who want to run RSS Bridge on [Heroku](https://heroku.com). 2 | 3 | You can run it on Heroku for free, however make sure your RSS reader interval is not set to a fast rate, otherwise your Heroku hours will use up quicker. Heroku puts the app to sleep after 30 mins of no activity. When the app is asleep no Heroku hours are used. So choose an interval that won't make the app exceed the limit! 4 | 5 | You can increase your Heroku hours to 1000 a month from 550 a month by simply verifying your account with a credit card. 6 | 7 | ## Deploy button 8 | You can simply press the button below to easily deploy RSS Bridge on Heroku and use the default bridges. Or you can follow the manual instructions given below. 9 | 10 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/RSS-Bridge/rss-bridge) 11 | 12 | ## Manual deploy 13 | 1. Fork this repo by clicking the Fork button at the top right of this page (only on desktop site) 14 | 15 | ![image](../images/fork_button.png) 16 | 17 | 2. To customise what bridges can be used if need, see [here](../03_For_Hosts/05_Whitelisting.md). You don’t need to do this if you’re fine with the default bridges. 18 | 19 | 3. [Log in to Heroku](https://dashboard.heroku.com) and create a new app. The app name will be the URL of the RSS Bridge (appname.herokuapp.com) 20 | 21 | 4. Go to Deploy, select the GitHub option and connect your GitHub account. Search for the repo named `yourusername/rss-bridge` 22 | 23 | ![image](../images/heroku_deploy.png) 24 | 25 | 5. Deploy the master branch. -------------------------------------------------------------------------------- /bridges/StanfordSIRbookreviewBridge.php: -------------------------------------------------------------------------------- 1 | [ 12 | 'name' => 'style', 13 | 'type' => 'list', 14 | 'values' => [ 15 | 'reviews' => 'reviews', 16 | 'excerpts' => 'excerpts', 17 | ] 18 | ] 19 | ] 20 | ]; 21 | 22 | public function collectData() 23 | { 24 | switch ($this->getInput('style')) { 25 | case 'reviews': 26 | $url = self::URI . 'reviews'; 27 | break; 28 | case 'excerpts': 29 | $url = self::URI . 'excerpts'; 30 | break; 31 | } 32 | 33 | $html = getSimpleHTMLDOM($url) 34 | or returnServerError('Failed loading content!'); 35 | foreach ($html->find('article') as $element) { 36 | $item = []; 37 | $item['title'] = $element->find('div > h4 > a', 0)->plaintext; 38 | $item['uri'] = $element->find('div > h4 > a', 0)->href; 39 | $item['content'] = $element->find('div > div.article-entry > p', 2)->plaintext; 40 | $item['author'] = $element->find('div > div > p', 0)->plaintext; 41 | $this->items[] = $item; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /bridges/KhinsiderBridge.php: -------------------------------------------------------------------------------- 1 | find('.latestSoundtrackHeading'); 16 | $tables = $html->find('.albumList'); 17 | // $dates is empty 18 | foreach ($dates as $i => $date) { 19 | $item = []; 20 | $item['uri'] = self::URI; 21 | $item['timestamp'] = DateTime::createFromFormat('F jS, Y', $date->plaintext)->setTime(1, 1)->format('U'); 22 | $item['title'] = sprintf('OST for %s', $date->plaintext); 23 | $item['author'] = 'Khinsider'; 24 | $trs = $tables[$i]->find('tr'); 25 | $content = '

    '; 26 | foreach ($trs as $tr) { 27 | $td = $tr->find('td', 1); 28 | if (null !== $td) { 29 | $link = $td->find('a', 0); 30 | $content .= sprintf('
  • %s
  • ', $link->href, $link->plaintext); 31 | } 32 | } 33 | $content .= '
'; 34 | $item['content'] = $content; 35 | $item['uid'] = $item['timestamp']; 36 | $item['categories'] = ['Video games', 'Music', 'OST', 'download']; 37 | 38 | $this->items[] = $item; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /bridges/RaceDepartmentBridge.php: -------------------------------------------------------------------------------- 1 | collectExpandableDatas('https://www.racedepartment.com/ams/index.rss', 10); 14 | } 15 | 16 | protected function parseItem(array $item) 17 | { 18 | $articlePage = getSimpleHTMLDOMCached($item['uri']); 19 | 20 | $coverImage = $articlePage->find('img.js-articleCoverImage', 0); 21 | #relative url -> absolute url 22 | $coverImage = str_replace('src="/', 'src="' . $this->getURI() . '/', $coverImage); 23 | $article = $articlePage->find('article.articleBody-main > div.bbWrapper', 0); 24 | $item['content'] = str_get_html($coverImage . $article); 25 | 26 | //convert iframes to links. meant for embedded videos. 27 | foreach ($item['content']->find('iframe') as $found) { 28 | $iframeUrl = $found->getAttribute('src'); 29 | 30 | if ($iframeUrl) { 31 | $found->outertext = '' . $iframeUrl . ''; 32 | } 33 | } 34 | 35 | $item['categories'] = []; 36 | foreach ($articlePage->find('a.tagItem') as $tag) { 37 | array_push($item['categories'], $tag->innertext); 38 | } 39 | 40 | return $item; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /bridges/FirefoxReleaseNotesBridge.php: -------------------------------------------------------------------------------- 1 | [ 12 | 'name' => 'Platform', 13 | 'type' => 'list', 14 | 'values' => [ 15 | 'Desktop' => '', 16 | 'Beta' => 'beta', 17 | 'Nightly' => 'nightly', 18 | 'Android' => 'android', 19 | 'iOS' => 'ios', 20 | ] 21 | ] 22 | ] 23 | ]; 24 | 25 | public function getName() 26 | { 27 | $platform = $this->getKey('platform'); 28 | return sprintf('Firefox %s Release Notes', $platform ?? ''); 29 | } 30 | 31 | public function collectData() 32 | { 33 | $platform = $this->getKey('platform'); 34 | $url = self::URI . $this->getInput('platform') . '/notes/'; 35 | $dom = getSimpleHTMLDOM($url); 36 | 37 | $version = $dom->find('.c-release-version', 0)->innertext; 38 | 39 | $this->items[] = [ 40 | 'content' => $dom->find('.c-release-notes', 0)->innertext, 41 | 'timestamp' => $dom->find('.c-release-date', 0)->innertext, 42 | 'title' => sprintf('Firefox %s %s Release Note', $platform, $version), 43 | 'uri' => $url, 44 | 'uid' => $platform . $version, 45 | ]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /bridges/ScmbBridge.php: -------------------------------------------------------------------------------- 1 | find('article') as $article) { 17 | $item = []; 18 | $item['uri'] = self::URI . $article->find('p.summary a', 0)->href; 19 | $item['title'] = $article->find('header h1 a', 0)->innertext; 20 | 21 | // remove text "En savoir plus" from anecdote content 22 | $readMoreButton = $article->find('span.read-more', 0); 23 | if ($readMoreButton) { 24 | $readMoreButton->outertext = ''; 25 | } 26 | $content = $article->find('p.summary a', 0)->innertext; 27 | 28 | // remove superfluous spaces at the end 29 | $content = substr($content, 0, strlen($content) - 17); 30 | 31 | // get publication date 32 | $str_date = $article->find('time', 0)->datetime; 33 | [$date, $time] = explode(' ', $str_date); 34 | [$y, $m, $d] = explode('-', $date); 35 | [$h, $i] = explode(':', $time); 36 | $timestamp = mktime($h, $i, 0, $m, $d, $y); 37 | $item['timestamp'] = $timestamp; 38 | 39 | $item['content'] = $content; 40 | $this->items[] = $item; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /templates/frontpage.html.php: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 28 | 29 | 30 | 31 | 60 | -------------------------------------------------------------------------------- /bridges/TebeoBridge.php: -------------------------------------------------------------------------------- 1 | [ 13 | 'name' => 'Catégorie', 14 | 'type' => 'list', 15 | 'values' => [ 16 | 'Toutes les vidéos' => '/', 17 | 'Actualité' => '/14-actualite', 18 | 'Sport' => '/3-sport', 19 | 'Culture-Loisirs' => '/5-culture-loisirs', 20 | 'Société' => '/15-societe', 21 | 'Langue Bretonne' => '/9-langue-bretonne' 22 | ] 23 | ] 24 | ]]; 25 | 26 | public function getIcon() 27 | { 28 | return self::URI . 'images/header_logo.png'; 29 | } 30 | 31 | public function collectData() 32 | { 33 | $url = self::URI . '/le-replay/' . $this->getInput('cat'); 34 | $html = getSimpleHTMLDOM($url); 35 | 36 | foreach ($html->find('div[id=items_replay] div.replay') as $element) { 37 | $item = []; 38 | $item['uri'] = $element->find('a', 0)->href; 39 | $item['title'] = $element->find('h3', 0)->plaintext; 40 | $item['timestamp'] = strtotime($element->find('p.moment-format-day', 0)->plaintext); 41 | $item['content'] = ''; 42 | $this->items[] = $item; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /bridges/PanacheDigitalGamesBridge.php: -------------------------------------------------------------------------------- 1 | getURI(); 25 | $html = getSimpleHTMLDOMCached($articles); 26 | 27 | foreach ($html->find('.news-item') as $element) { 28 | $item = []; 29 | 30 | $title = $element->find('.news-item-texts-title', 0); 31 | $link = $element->find('.news-item-texts a', 0); 32 | $timestamp = $element->find('.news-item-texts-date', 0); 33 | 34 | $item['title'] = $title->plaintext; 35 | $item['uri'] = self::URI . $link->href; 36 | $item['timestamp'] = strtotime($timestamp->plaintext); 37 | 38 | $image_html = $element->find('.news-item-thumbnail-image', 0); 39 | if ($image_html) { 40 | $image_strings = explode('\'', $image_html); 41 | 42 | if (count($image_strings) == 4) { 43 | $item['content'] = ''; 44 | } 45 | } 46 | 47 | $this->items[] = $item; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /bridges/FiaBridge.php: -------------------------------------------------------------------------------- 1 | find('li.document-row'); 16 | foreach ($items as $item) { 17 | /** @var simple_html_dom $item */ 18 | // Do something with each list item 19 | $title = trim($item->find('div.title', 0)->plaintext); 20 | $href = $item->find('a', 0)->href; 21 | $url = 'https://www.fia.com' . $href; 22 | 23 | $date = $item->find('span.date-display-single', 0)->plaintext; 24 | 25 | $item = []; 26 | $item['uri'] = $url; 27 | $item['title'] = $title; 28 | $item['timestamp'] = (string) DateTime::createFromFormat('d.m.y H:i', $date)->getTimestamp(); 29 | ; 30 | $item['author'] = 'Fia'; 31 | $item['content'] = "Document on date $date: $title
$url"; 32 | $item['categories'] = 'Document'; 33 | $item['uid'] = $title . $date; 34 | 35 | $count = count($this->items); 36 | if ($count > 20) { 37 | break; 38 | } else { 39 | $this->items[] = $item; 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /bridges/EDDHPresseschauBridge.php: -------------------------------------------------------------------------------- 1 | setTime(0, 0, 0); 40 | return $dti->getTimestamp(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /bridges/VproTegenlichtBridge.php: -------------------------------------------------------------------------------- 1 | find('ul#browsable-news-overview', 0); 22 | $dom = defaultLinkTo($dom, $this->getURI()); 23 | foreach ($dom->find('li') as $article) { 24 | $a = $article->find('a.complex-teaser', 0); 25 | $title = $article->find('a.complex-teaser', 0)->title; 26 | $url = $article->find('a.complex-teaser', 0)->href; 27 | $author = 'VPRO tegenlicht'; 28 | $content = $article->find('p.complex-teaser-summary', 0)->plaintext; 29 | $timestamp = strtotime($article->find('div.complex-teaser-data', 0)->plaintext); 30 | 31 | $item = [ 32 | 'uri' => $url, 33 | 'author' => $author, 34 | 'title' => $title, 35 | 'timestamp' => $timestamp, 36 | 'content' => $content 37 | ]; 38 | 39 | $this->items[] = $item; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /docs/01_General/05_FAQ.md: -------------------------------------------------------------------------------- 1 | ## Why doesn't my bridge show new contents? 2 | 3 | RSS-Bridge creates a cached version of your feed in order to reduce traffic and respond faster. 4 | The cached version is created on the first request and served for all subsequent requests. 5 | On every request RSS-Bridge checks if the cache timeout has elapsed. 6 | If the timeout has elapsed, it loads new contents and updates the cached version. 7 | 8 | _Notice_: RSS-Bridge only updates feeds if you actively request it, 9 | for example by pressing F5 in your browser or using a feed reader. 10 | 11 | The cache duration is bridge specific (usually `1h`) 12 | You can specify a custom cache timeout for each bridge if 13 | [this option](#how-can-i-make-a-bridge-update-more-frequently) has been enabled on the server. 14 | 15 | ## How can I make a bridge update more frequently? 16 | 17 | You can only do that if you are hosting the RSS-Bridge instance: 18 | - Lower the bridge ttl: `CACHE_TIMEOUT` constant 19 | - Enable [`custom_timeout`](../03_For_Hosts/08_Custom_Configuration.md#customtimeout) 20 | 21 | ## Firefox doesn't show feeds anymore, what can I do? 22 | 23 | As of version 64, Firefox removed support for viewing Atom and RSS feeds in the browser. 24 | This results in the browser downloading the pages instead of showing contents. 25 | 26 | Further reading: 27 | - https://support.mozilla.org/en-US/kb/feed-reader-replacements-firefox 28 | - https://bugzilla.mozilla.org/show_bug.cgi?id=1477667 29 | 30 | To restore the original behavior in Firefox 64 or higher you can use following Add-on 31 | which attempts to recreate the original behavior (with some sugar on top): 32 | 33 | - https://addons.mozilla.org/en-US/firefox/addon/rsspreview/ 34 | -------------------------------------------------------------------------------- /bridges/TheFarSideBridge.php: -------------------------------------------------------------------------------- 1 | find('div.tfs-page-container__cows', 0); 18 | 19 | $item = []; 20 | $item['uri'] = $html->find('meta[property="og:url"]', 0)->content; 21 | $item['title'] = $div->find('h3', 0)->innertext; 22 | $item['timestamp'] = $div->find('h3', 0)->innertext; 23 | $item['content'] = ''; 24 | 25 | foreach ($div->find('div.card-body') as $index => $card) { 26 | $image = $card->find('img', 0); 27 | $imageUrl = $image->attr['data-src']; 28 | 29 | // Images are downloaded to bypass the hotlink protection. 30 | $image = getContents($imageUrl, ['Referer: ' . self::URI]); 31 | 32 | // Encode image as base64 33 | $imageBase64 = base64_encode($image); 34 | 35 | $caption = ''; 36 | 37 | if ($card->find('figcaption', 0)) { 38 | $caption = $card->find('figcaption', 0)->innertext; 39 | } 40 | 41 | $item['content'] .= << 43 | 44 |
{$caption}
45 | 46 |
47 | EOD; 48 | } 49 | 50 | $this->items[] = $item; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /bridges/IdenticaBridge.php: -------------------------------------------------------------------------------- 1 | [ 13 | 'name' => 'username', 14 | 'exampleValue' => 'jxself', 15 | 'required' => true 16 | ] 17 | ]]; 18 | 19 | public function collectData() 20 | { 21 | $html = getSimpleHTMLDOM($this->getURI()); 22 | 23 | foreach ($html->find('li.major') as $dent) { 24 | $item = []; 25 | 26 | // get dent link 27 | $item['uri'] = html_entity_decode($dent->find('a', 0)->href); 28 | 29 | // extract dent timestamp 30 | $item['timestamp'] = strtotime($dent->find('abbr.easydate', 0)->plaintext); 31 | 32 | // extract dent text 33 | $item['content'] = trim($dent->find('div.activity-content', 0)->innertext); 34 | $item['title'] = $this->getInput('u') . ' | ' . $item['content']; 35 | $this->items[] = $item; 36 | } 37 | } 38 | 39 | public function getName() 40 | { 41 | if (!is_null($this->getInput('u'))) { 42 | return $this->getInput('u') . ' - Identica Bridge'; 43 | } 44 | 45 | return parent::getName(); 46 | } 47 | 48 | public function getURI() 49 | { 50 | if (!is_null($this->getInput('u'))) { 51 | return self::URI . urlencode($this->getInput('u')); 52 | } 53 | 54 | return parent::getURI(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /bridges/WallmineNewsBridge.php: -------------------------------------------------------------------------------- 1 | getURI() . '/news/'); 16 | 17 | $html = defaultLinkTo($html, self::URI); 18 | 19 | foreach ($html->find('div.container.news-card') as $div) { 20 | $item = []; 21 | $item['uri'] = $div->find('a', 0)->href; 22 | 23 | $image = $div->find('img.img-fluid', 0)->src; 24 | 25 | $page = getSimpleHTMLDOMCached($item['uri'], 7200); 26 | 27 | $article = $page->find('div.container.article-container', 0); 28 | 29 | $item['title'] = $article->find('h1', 0)->plaintext; 30 | 31 | $article->find('p.published-on', 0)->children(0)->outertext = ''; 32 | $article->find('p.published-on', 0)->children(1)->outertext = ''; 33 | $date = str_replace('at', '', $article->find('p.published-on', 0)->innertext); 34 | 35 | $item['timestamp'] = $date; 36 | 37 | $article->find('h1', 0)->outertext = ''; 38 | $article->find('p.published-on', 0)->outertext = ''; 39 | 40 | $item['content'] = $article->innertext; 41 | $item['enclosures'][] = $image; 42 | 43 | $this->items[] = $item; 44 | 45 | if (count($this->items) >= 10) { 46 | break; 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/02_CLI/index.md: -------------------------------------------------------------------------------- 1 | RSS-Bridge supports calls via CLI. 2 | You can use the same parameters as you would normally use via the URI. Example: 3 | 4 | `php index.php action=display bridge=DansTonChat format=Json` 5 | 6 | ## Required parameters 7 | 8 | RSS-Bridge requires a few parameters that must be specified on every call. 9 | Omitting these parameters will result in error messages: 10 | 11 | ### action 12 | 13 | Defines how RSS-Bridge responds to the request. 14 | 15 | Value | Description 16 | ----- | ----------- 17 | `action=list` | Returns a JSON formatted list of bridges. Other parameters are ignored. 18 | `action=display` | Returns (displays) a feed. 19 | 20 | ### bridge 21 | 22 | This parameter specifies the name of the bridge RSS-Bridge should return feeds from. 23 | The name of the bridge equals the class name of the bridges in the ./bridges/ folder without the 'Bridge' prefix. 24 | For example: DansTonChatBridge => DansTonChat. 25 | 26 | ### format 27 | 28 | This parameter specifies the format in which RSS-Bridge returns the contents. 29 | 30 | ## Optional parameters 31 | 32 | RSS-Bridge supports optional parameters. 33 | These parameters are only valid if the options have been enabled in the index.php script. 34 | 35 | ### \_noproxy 36 | 37 | This parameter is only available if a proxy server has been specified via `proxy.url` and `proxy.by_bridge` 38 | has been enabled. This is a Boolean parameter that can be set to `true` or `false`. 39 | 40 | ## Bridge parameters 41 | 42 | Each bridge can specify its own set of parameters. 43 | As in the example above, some bridges don't specify any parameters or only optional parameters that can be neglected. 44 | For more details read the `PARAMETERS` definition for your bridge. 45 | -------------------------------------------------------------------------------- /bridges/PlantUMLReleasesBridge.php: -------------------------------------------------------------------------------- 1 | getURI()), self::URI); 27 | 28 | $num_items = 0; 29 | $main = $html->find('div[id=root]', 0); 30 | foreach ($main->find('h2') as $release) { 31 | // Limit to $ITEM_LIMIT number of results 32 | if ($num_items++ >= self::ITEM_LIMIT) { 33 | break; 34 | } 35 | $item = []; 36 | $item['author'] = self::AUTHOR; 37 | $release_text = $release->innertext; 38 | if (preg_match('/(.+) \((.*)\)/', $release_text, $matches)) { 39 | $item['title'] = $matches[1]; 40 | $item['timestamp'] = preg_replace('/(\d+) (\w{3})\w*, (\d+)/', '${1} ${2} ${3}', $matches[2]); 41 | } else { 42 | $item['title'] = $release_text; 43 | } 44 | $item['uri'] = $this->getURI(); 45 | $item['content'] = $release->next_sibling(); 46 | $this->items[] = $item; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/FormatFactory.php: -------------------------------------------------------------------------------- 1 | getFilename(), $m)) { 12 | $this->formatNames[] = $m[1]; 13 | } 14 | } 15 | sort($this->formatNames); 16 | } 17 | 18 | public function create(string $name): FormatAbstract 19 | { 20 | if (! preg_match('/^[a-zA-Z0-9-]*$/', $name)) { 21 | throw new \InvalidArgumentException('Format name invalid!'); 22 | } 23 | $sanitizedName = $this->sanitizeName($name); 24 | if (!$sanitizedName) { 25 | throw new \InvalidArgumentException(sprintf('Unknown format given `%s`', $name)); 26 | } 27 | $className = '\\' . $sanitizedName . 'Format'; 28 | return new $className(); 29 | } 30 | 31 | public function getFormatNames(): array 32 | { 33 | return $this->formatNames; 34 | } 35 | 36 | protected function sanitizeName(string $name): ?string 37 | { 38 | $name = ucfirst(strtolower($name)); 39 | // Trim trailing '.php' if exists 40 | if (preg_match('/(.+)(?:\.php)/', $name, $matches)) { 41 | $name = $matches[1]; 42 | } 43 | // Trim trailing 'Format' if exists 44 | if (preg_match('/(.+)(?:Format)/i', $name, $matches)) { 45 | $name = $matches[1]; 46 | } 47 | if (in_array($name, $this->formatNames)) { 48 | return $name; 49 | } 50 | return null; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.devcontainer/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Listen for Xdebug", 9 | "type": "php", 10 | "request": "launch", 11 | "port": 9003, 12 | "auto": true 13 | }, 14 | { 15 | "name": "Launch currently open script", 16 | "type": "php", 17 | "request": "launch", 18 | "program": "${file}", 19 | "cwd": "${fileDirname}", 20 | "port": 0, 21 | "runtimeArgs": [ 22 | "-dxdebug.start_with_request=yes" 23 | ], 24 | "env": { 25 | "XDEBUG_MODE": "debug,develop", 26 | "XDEBUG_CONFIG": "client_port=${port}" 27 | } 28 | }, 29 | { 30 | "name": "Launch Built-in web server", 31 | "type": "php", 32 | "request": "launch", 33 | "runtimeArgs": [ 34 | "-dxdebug.mode=debug", 35 | "-dxdebug.start_with_request=yes", 36 | "-S", 37 | "localhost:0" 38 | ], 39 | "program": "", 40 | "cwd": "${workspaceRoot}", 41 | "port": 9003, 42 | "serverReadyAction": { 43 | "pattern": "Development Server \\(http://localhost:([0-9]+)\\) started", 44 | "uriFormat": "http://localhost:%s", 45 | "action": "openExternally" 46 | } 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /docs/10_Bridge_Specific/Instagram.md: -------------------------------------------------------------------------------- 1 | InstagramBridge 2 | =============== 3 | 4 | To somehow bypass the [rate limiting issue](https://github.com/RSS-Bridge/rss-bridge/issues/1891) 5 | it is suggested to deploy a private RSS-Bridge instance that uses a working Instagram account. 6 | 7 | **NOTE**: There exists alternative bridges (e.g. PicukiBridge and PicnobBridge) for viewing posts without a working account. 8 | 9 | Configuration 10 | ------------- 11 | 12 | 1. Retreiving `session id` and `ds_user_id`. 13 | The following steps describe how to get the `session id` and `ds user id` using a Chromium-based browser. 14 | 15 | - Create an Instagram account, that you will use for your RSS-Bridge instance. 16 | It is NOT recommended to use your existing account that is used for common interaction with Instagram services. 17 | 18 | - Login to Instagram 19 | 20 | - Open DevTools by pressing F12 21 | 22 | - Open "Networks tab" 23 | 24 | - In the "Filter" field input "i.instagram.com" 25 | 26 | - Click on "Fetch/XHR" 27 | 28 | - Refresh web page 29 | 30 | - Click on any item from the table of http requests 31 | 32 | - In the new frame open the "Headers" tab and scroll to "Request Headers" 33 | 34 | - There will be a cookie param will lots of `=;` text. You need the value of the "sessionid" and "ds_user_id" keys. Copy them. 35 | 36 | 2. Configuring RSS-Bridge 37 | 38 | - In config.ini.php add following configuration: 39 | 40 | ``` 41 | [InstagramBridge] 42 | session_id = %sessionid from step 1% 43 | ds_user_id = %ds_user_id from step 1% 44 | cache_timeout = %cache timeout in seconds% 45 | ``` 46 | 47 | The bigger the cache_timeout value, the smaller the chance for RSS-Bridge to throw 429 errors. 48 | Default cache_timeout is 3600 seconds (1 hour). 49 | -------------------------------------------------------------------------------- /bridges/SuperSmashBlogBridge.php: -------------------------------------------------------------------------------- 1 | '); 23 | } else { 24 | $picture = ''; 25 | } 26 | 27 | $video = $article['acf']['link_url']; 28 | if (strlen($video) != 0) { 29 | $video = str_get_html('Youtube video'); 30 | } else { 31 | $video = ''; 32 | } 33 | $text = str_get_html($article['acf']['editor']); 34 | $content = $picture . $video . $text; 35 | 36 | // Build final item 37 | $item = []; 38 | $item['title'] = $article['title']['rendered']; 39 | $item['timestamp'] = strtotime($article['date']); 40 | $item['content'] = $content; 41 | $item['uri'] = self::URI . '?post=' . $article['id']; 42 | 43 | $this->items[] = $item; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /bridges/DuckDuckGoBridge.php: -------------------------------------------------------------------------------- 1 | [ 16 | 'name' => 'keyword', 17 | 'exampleValue' => 'duck', 18 | 'required' => true 19 | ], 20 | 'sort' => [ 21 | 'name' => 'sort by', 22 | 'type' => 'list', 23 | 'required' => false, 24 | 'values' => [ 25 | 'date' => self::SORT_DATE, 26 | 'relevance' => self::SORT_RELEVANCE 27 | ], 28 | 'defaultValue' => self::SORT_DATE 29 | ] 30 | ]]; 31 | 32 | public function collectData() 33 | { 34 | $query = [ 35 | 'kd' => '-1', 36 | 'q' => $this->getInput('u') . $this->getInput('sort'), 37 | ]; 38 | $url = 'https://duckduckgo.com/html/?' . http_build_query($query); 39 | $html = getSimpleHTMLDOM($url); 40 | 41 | foreach ($html->find('div.result') as $element) { 42 | $item = []; 43 | $item['uri'] = $element->find('a.result__a', 0)->href; 44 | $item['title'] = $element->find('h2.result__title', 0)->plaintext; 45 | 46 | $snippet = $element->find('a.result__snippet', 0); 47 | if ($snippet) { 48 | $item['content'] = $snippet->plaintext; 49 | } 50 | $this->items[] = $item; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /docs/10_Bridge_Specific/Vk2.md: -------------------------------------------------------------------------------- 1 | Vk2Bridge 2 | ========= 3 | 4 | Работа этого скрипта основана [VK API](https://dev.vk.com/reference). 5 | По сравнению с VkBridge у этого скрипта есть свои приемущества и недостатки. 6 | 7 | Приемущества 8 | ------------ 9 | 10 | - Стабильность. 11 | Скрипт не зависит от HTML-структуры страницы VK групп или пользователей, которые могут поменяться в любой момент. 12 | 13 | Недостатки 14 | ---------- 15 | 16 | - Требуется наличие зарегистированного в ВК пользователя. 17 | Данный пользователь должен получить `access_token`, который используется для этого скрипта. 18 | Подробнее в разделе "Настройка" 19 | 20 | - Количество запросов при выключенном кэше ограничено - [5000 запросов в сутки](https://dev.vk.com/ru/reference/roadmap#%D0%9E%D0%B3%D1%80%D0%B0%D0%BD%D0%B8%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%20API%20%D0%B4%D0%BB%D1%8F%20%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0) 21 | 22 | Настройка 23 | --------- 24 | 25 | 1. Перейдите по [ссылке](https://oauth.vk.com/oauth/authorize?client_id=5149410&scope=offline&redirect_uri=https://oauth.vk.com/blank.html&display=page&response_type=token) 26 | 27 | 2. Авторизуйтесь в приложение `my_personal_app` 28 | 29 | 3. Получите ссылку вида `https://oauth.vk.com/blank.html#access_token=MNOGO_BUKAV&expires_in=0&user_id=123456`. 30 | Из этой ссылки скопируйте `MNOGO_BUKAV`. 31 | 32 | 4. В `config.ini.php` в раздел Vk2Bridge вставьте `access_token` 33 | 34 | ``` 35 | [Vk2Bridge] 36 | access_token = "MNOGO_BUKAV" 37 | ``` 38 | 39 | Примечание: в данной инструкции используется приложение, администратор которого является [@em92](https://github.com/em92). 40 | Допускается вместо упомянутого приложения использование своего standalone-приложения. 41 | Для этого надо в ссылке из п.1. заменить значение `client_id` на свой. 42 | -------------------------------------------------------------------------------- /bridges/TinyLetterBridge.php: -------------------------------------------------------------------------------- 1 | [ 12 | 'name' => 'User Name', 13 | 'required' => true, 14 | 'exampleValue' => 'forwards', 15 | ] 16 | ] 17 | ]; 18 | 19 | public function getName() 20 | { 21 | $username = $this->getInput('username'); 22 | if (!is_null($username)) { 23 | return static::NAME . ' | ' . $username; 24 | } 25 | 26 | return parent::getName(); 27 | } 28 | 29 | public function getURI() 30 | { 31 | $username = $this->getInput('username'); 32 | if (!is_null($username)) { 33 | return static::URI . urlencode($username); 34 | } 35 | 36 | return parent::getURI(); 37 | } 38 | 39 | public function collectData() 40 | { 41 | $archives = $this->getURI() . '/archive'; 42 | $html = getSimpleHTMLDOMCached($archives); 43 | 44 | foreach ($html->find('.message-list li') as $element) { 45 | $item = []; 46 | 47 | $snippet = $element->find('p.message-snippet', 0); 48 | $link = $element->find('.message-link', 0); 49 | 50 | $item['title'] = $link->plaintext; 51 | $item['content'] = $snippet->innertext; 52 | $item['uri'] = $link->href; 53 | $item['timestamp'] = strtotime($element->find('.message-date', 0)->plaintext); 54 | 55 | $this->items[] = $item; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /bridges/WallpaperflareBridge.php: -------------------------------------------------------------------------------- 1 | [ 11 | 'search' => [ 12 | 'name' => 'Search', 13 | 'exampleValue' => 'birds', 14 | 'required' => true 15 | ] 16 | ]]; 17 | const CACHE_TIMEOUT = 3600; //1 hour 18 | const XPATH_EXPRESSION_ITEM = './/figure'; 19 | const XPATH_EXPRESSION_ITEM_TITLE = './/img/@title'; 20 | const XPATH_EXPRESSION_ITEM_CONTENT = ''; 21 | const XPATH_EXPRESSION_ITEM_URI = './/a[@itemprop="url"]/@href'; 22 | const XPATH_EXPRESSION_ITEM_AUTHOR = '/html[1]/body[1]/main[1]/section[1]/h1[1]'; 23 | const XPATH_EXPRESSION_ITEM_TIMESTAMP = ''; 24 | const XPATH_EXPRESSION_ITEM_ENCLOSURES = './/img/@data-src'; 25 | const XPATH_EXPRESSION_ITEM_CATEGORIES = './/figcaption[@itemprop="caption description"]'; 26 | const SETTING_FIX_ENCODING = false; 27 | 28 | protected function getSourceUrl() 29 | { 30 | return 'https://www.wallpaperflare.com/search?wallpaper=' . $this->getInput('search'); 31 | } 32 | 33 | public function getIcon() 34 | { 35 | return 'https://www.google.com/s2/favicons?domain=wallpaperflare.com/'; 36 | } 37 | 38 | public function getName() 39 | { 40 | if (!is_null($this->getInput('search'))) { 41 | return 'Wallpaperflare - ' . $this->getInput('search'); 42 | } else { 43 | return 'Wallpaperflare'; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /bridges/SummitsOnTheAirBridge.php: -------------------------------------------------------------------------------- 1 | [ 13 | 'c' => [ 14 | 'name' => 'count', 15 | 'required' => true, 16 | 'defaultValue' => 10 17 | ] 18 | ] 19 | ]; 20 | 21 | public function collectData() 22 | { 23 | $header = [ 24 | 'Content-type:application/json', 25 | ]; 26 | $opts = [ 27 | CURLOPT_HTTPGET => 1, 28 | ]; 29 | $json = getContents($this->getURI() . $this->getInput('c'), $header, $opts); 30 | 31 | $spots = json_decode($json, true); 32 | 33 | foreach ($spots as $spot) { 34 | $summit = $spot['associationCode'] . '/' . $spot['summitCode']; 35 | 36 | $title = $spot['activatorCallsign'] . ' @ ' . $summit . ' ' . 37 | $spot['frequency'] . ' MHz'; 38 | 39 | $content = << 41 | {$summit}, {$spot['summitDetails']}
42 | Frequency: {$spot['frequency']} MHz
43 | Mode: {$spot['mode']}
44 | Comments: {$spot['comments']} 45 | EOL; 46 | 47 | $this->items[] = [ 48 | 'uri' => 'https://sotawatch.sota.org.uk/en/', 49 | 'title' => $title, 50 | 'content' => $content, 51 | 'timestamp' => $spot['timeStamp'] 52 | ]; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /actions/DetectAction.php: -------------------------------------------------------------------------------- 1 | bridgeFactory = $bridgeFactory; 11 | } 12 | 13 | public function __invoke(Request $request): Response 14 | { 15 | $url = $request->get('url'); 16 | $format = $request->get('format'); 17 | 18 | if (!$url) { 19 | return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'You must specify a url'])); 20 | } 21 | if (!$format) { 22 | return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'You must specify a format'])); 23 | } 24 | 25 | foreach ($this->bridgeFactory->getBridgeClassNames() as $bridgeClassName) { 26 | if (!$this->bridgeFactory->isEnabled($bridgeClassName)) { 27 | continue; 28 | } 29 | 30 | $bridge = $this->bridgeFactory->create($bridgeClassName); 31 | 32 | $bridgeParams = $bridge->detectParameters($url); 33 | 34 | if (!$bridgeParams) { 35 | continue; 36 | } 37 | 38 | $query = [ 39 | 'action' => 'display', 40 | 'bridge' => $bridgeClassName, 41 | 'format' => $format, 42 | ]; 43 | $query = array_merge($query, $bridgeParams); 44 | return new Response('', 301, ['location' => '?' . http_build_query($query)]); 45 | } 46 | 47 | return new Response(render(__DIR__ . '/../templates/error.html.php', [ 48 | 'message' => 'No bridge found for given URL: ' . $url, 49 | ])); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /bridges/ABCNewsBridge.php: -------------------------------------------------------------------------------- 1 | [ 13 | 'type' => 'list', 14 | 'name' => 'Region', 15 | 'title' => 'Choose state', 16 | 'values' => [ 17 | 'ACT' => 'act', 18 | 'NSW' => 'nsw', 19 | 'NT' => 'nt', 20 | 'QLD' => 'qld', 21 | 'SA' => 'sa', 22 | 'TAS' => 'tas', 23 | 'VIC' => 'vic', 24 | 'WA' => 'wa' 25 | ], 26 | ] 27 | ] 28 | ]; 29 | 30 | public function collectData() 31 | { 32 | $url = sprintf('https://www.abc.net.au/news/%s', $this->getInput('topic')); 33 | $dom = getSimpleHTMLDOM($url); 34 | $dom = $dom->find('div[data-component="PaginationList"]', 0); 35 | if (!$dom) { 36 | throw new \Exception(sprintf('Unable to find css selector on `%s`', $url)); 37 | } 38 | $dom = defaultLinkTo($dom, $this->getURI()); 39 | foreach ($dom->find('article[data-component="DetailCard"]') as $article) { 40 | $a = $article->find('a', 0); 41 | $this->items[] = [ 42 | 'title' => $a->plaintext, 43 | 'uri' => $a->href, 44 | 'content' => $article->find('p', 0)->plaintext, 45 | 'timestamp' => strtotime($article->find('time', 0)->datetime), 46 | ]; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /formats/SfeedFormat.php: -------------------------------------------------------------------------------- 1 | getItems() as $item) { 11 | $text .= sprintf( 12 | "%s\t%s\t%s\t%s\thtml\t\t%s\t%s\t%s\n", 13 | $item->toArray()['timestamp'], 14 | preg_replace('/\s/', ' ', $item->toArray()['title']), 15 | $item->toArray()['uri'], 16 | $this->escape($item->toArray()['content']), 17 | $item->toArray()['author'], 18 | $this->getFirstEnclosure( 19 | $item->toArray()['enclosures'] 20 | ), 21 | $this->escape( 22 | $this->getCategories( 23 | $item->toArray()['categories'] 24 | ) 25 | ) 26 | ); 27 | } 28 | 29 | return $text; 30 | } 31 | 32 | private function escape(string $str) 33 | { 34 | $str = str_replace('\\', '\\\\', $str); 35 | $str = str_replace("\n", '\\n', $str); 36 | return str_replace("\t", '\\t', $str); 37 | } 38 | 39 | private function getFirstEnclosure(array $enclosures) 40 | { 41 | if (count($enclosures) >= 1) { 42 | return $enclosures[0]; 43 | } 44 | return ''; 45 | } 46 | 47 | private function getCategories(array $cats) 48 | { 49 | $toReturn = ''; 50 | $i = 1; 51 | foreach ($cats as $cat) { 52 | $toReturn .= trim($cat); 53 | if (count($cats) > $i++) { 54 | $toReturn .= '|'; 55 | } 56 | } 57 | return $toReturn; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /bridges/ElsevierBridge.php: -------------------------------------------------------------------------------- 1 | [ 13 | 'name' => 'Journal name', 14 | 'required' => true, 15 | 'exampleValue' => 'academic-pediatrics', 16 | 'title' => 'Insert html-part of your journal' 17 | ] 18 | ]]; 19 | 20 | public function collectData() 21 | { 22 | // Not all journals have the /recent-articles page 23 | $url = sprintf('https://www.journals.elsevier.com/%s/recent-articles/', $this->getInput('j')); 24 | $html = getSimpleHTMLDOM($url); 25 | 26 | foreach ($html->find('article') as $recentArticle) { 27 | $item = []; 28 | $item['uri'] = $recentArticle->find('a', 0)->getAttribute('href'); 29 | $item['title'] = $recentArticle->find('h2', 0)->plaintext; 30 | $item['author'] = $recentArticle->find('p > span', 0)->plaintext; 31 | $publicationDateString = trim($recentArticle->find('p > span', 1)->plaintext); 32 | $publicationDate = DateTimeImmutable::createFromFormat('F d, Y', $publicationDateString); 33 | if ($publicationDate) { 34 | $item['timestamp'] = $publicationDate->getTimestamp(); 35 | } 36 | $this->items[] = $item; 37 | } 38 | } 39 | 40 | public function getIcon(): string 41 | { 42 | return 'https://cdn.elsevier.io/verona/includes/favicons/favicon-32x32.png'; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /bridges/SleeperFantasyFootballBridge.php: -------------------------------------------------------------------------------- 1 | find('div.content > div.latest-topics > a') as $index => $a) { 17 | $content = $a->find('div.title > p', 0)->innertext; 18 | $meta = $this->processString($a->find('div.desc > div.username', 0)->innertext); 19 | $item['title'] = $content; 20 | $item['content'] = $content; 21 | $item['categories'] = $a->find('div.title div.tag', 0)->innertext; 22 | $item['timestamp'] = $meta['timestamp']; 23 | $item['author'] = $meta['author']; 24 | $item['enclosures'] = $a->find('div.player-photo amp-img', 0)->src; 25 | $this->items[] = $item; 26 | if (count($this->items) >= 10) { 27 | break; 28 | } 29 | } 30 | } 31 | 32 | protected function processString($inputString) 33 | { 34 | $decodedString = str_replace([' ', '•'], [' ', '|'], $inputString); 35 | $splitArray = explode(' | ', $decodedString); 36 | $author = trim($splitArray[0]); 37 | $timeString = trim($splitArray[1]); 38 | $timestamp = strtotime($timeString); 39 | return [ 40 | 'author' => $author, 41 | 'timestamp' => $timestamp 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /bridges/ManyVidsBridge.php: -------------------------------------------------------------------------------- 1 | [ 13 | 'name' => 'Profile', 14 | 'type' => 'text', 15 | 'required' => true, 16 | 'exampleValue' => '678459/Aziani-Studios', 17 | 'title' => 'id/profile or url', 18 | ], 19 | ] 20 | ]; 21 | 22 | public function collectData() 23 | { 24 | $profile = $this->getInput('profile'); 25 | if (preg_match('#^(\d+/.*)$#', $profile, $m)) { 26 | $profile = $m[1]; 27 | } elseif (preg_match('#https://www.manyvids.com/Profile/(\d+/\w+)#', $profile, $m)) { 28 | $profile = $m[1]; 29 | } else { 30 | throw new \Exception('nope'); 31 | } 32 | $url = sprintf('https://www.manyvids.com/Profile/%s/Store/Videos/', $profile); 33 | $dom = getSimpleHTMLDOM($url); 34 | $videos = $dom->find('div[class^="ProfileTabGrid_card"]'); 35 | foreach ($videos as $item) { 36 | $a = $item->find('a', 1); 37 | $uri = 'https://www.manyvids.com' . $a->href; 38 | if (preg_match('#Video/(\d+)/#', $uri, $m)) { 39 | $uid = 'manyvids/' . $m[1]; 40 | } 41 | $this->items[] = [ 42 | 'title' => $a->plaintext, 43 | 'uri' => $uri, 44 | 'uid' => $uid ?? $uri, 45 | 'content' => $item->innertext, 46 | ]; 47 | } 48 | } 49 | } 50 | --------------------------------------------------------------------------------